@@ -9,14 +9,17 @@ |
||
9 | 9 |
#= require ./worker-checker |
10 | 10 |
#= require_self |
11 | 11 |
|
12 |
-window.setupJsonEditor = ($editor = $(".live-json-editor")) -> |
|
12 |
+window.setupJsonEditor = ($editors = $(".live-json-editor")) -> |
|
13 | 13 |
JSONEditor.prototype.ADD_IMG = '<%= image_path 'json-editor/add.png' %>' |
14 | 14 |
JSONEditor.prototype.DELETE_IMG = '<%= image_path 'json-editor/delete.png' %>' |
15 |
- if $editor.length |
|
15 |
+ editors = [] |
|
16 |
+ $editors.each -> |
|
17 |
+ $editor = $(this) |
|
16 | 18 |
jsonEditor = new JSONEditor($editor, $editor.data('width') || 400, $editor.data('height') || 500) |
17 | 19 |
jsonEditor.doTruncation true |
18 | 20 |
jsonEditor.showFunctionButtons() |
19 |
- return jsonEditor |
|
21 |
+ editors.push jsonEditor |
|
22 |
+ return editors |
|
20 | 23 |
|
21 | 24 |
hideSchedule = -> |
22 | 25 |
$(".schedule-region select").hide() |
@@ -55,12 +58,15 @@ showEventDescriptions = -> |
||
55 | 58 |
|
56 | 59 |
$(document).ready -> |
57 | 60 |
# JSON Editor |
58 |
- window.jsonEditor = setupJsonEditor() |
|
61 |
+ window.jsonEditor = setupJsonEditor()[0] |
|
59 | 62 |
|
60 | 63 |
# Flash |
61 | 64 |
if $(".flash").length |
62 | 65 |
setTimeout((-> $(".flash").slideUp(-> $(".flash").remove())), 5000) |
63 | 66 |
|
67 |
+ # Help popovers |
|
68 |
+ $('.hover-help').popover(trigger: 'hover') |
|
69 |
+ |
|
64 | 70 |
# Agent Navigation |
65 | 71 |
$agentNavigate = $('#agent-navigate') |
66 | 72 |
|
@@ -99,7 +105,7 @@ $(document).ready -> |
||
99 | 105 |
e.preventDefault() |
100 | 106 |
$agentNavigate.focus() |
101 | 107 |
|
102 |
-# Agent Show |
|
108 |
+ # Agent Show |
|
103 | 109 |
fetchLogs = (e) -> |
104 | 110 |
agentId = $(e.target).closest("[data-agent-id]").data("agent-id") |
105 | 111 |
e.preventDefault() |
@@ -140,13 +140,29 @@ span.not-applicable:after { |
||
140 | 140 |
opacity: 0.5; |
141 | 141 |
} |
142 | 142 |
|
143 |
-// Fix JSON Editor |
|
143 |
+// JSON Editor |
|
144 |
+ |
|
145 |
+.live-json-editor { |
|
146 |
+ font-family: "Courier New", Courier, monospace; |
|
147 |
+} |
|
144 | 148 |
|
145 | 149 |
.json-editor blockquote { |
146 | 150 |
font-size: 14px; |
147 | 151 |
} |
148 | 152 |
|
149 |
-// Bootstrappy colour styles |
|
153 |
+// Position tweeks |
|
154 |
+ |
|
155 |
+.hover-help { |
|
156 |
+ top: 2px; |
|
157 |
+} |
|
158 |
+ |
|
159 |
+h2 .scenario, a span.label.scenario { |
|
160 |
+ position: relative; |
|
161 |
+ top: -2px; |
|
162 |
+} |
|
163 |
+ |
|
164 |
+// Bootstrappy color styles |
|
165 |
+ |
|
150 | 166 |
.color-danger { |
151 | 167 |
color: #d9534f; |
152 | 168 |
} |
@@ -0,0 +1,15 @@ |
||
1 |
+.scenario-import { |
|
2 |
+ .agent-import-list { |
|
3 |
+ .agent-import { |
|
4 |
+ margin-bottom: 20px; |
|
5 |
+ |
|
6 |
+ .instructions { |
|
7 |
+ margin-bottom: 10px; |
|
8 |
+ } |
|
9 |
+ |
|
10 |
+ .current { |
|
11 |
+ font-weight: bold; |
|
12 |
+ } |
|
13 |
+ } |
|
14 |
+ } |
|
15 |
+} |
@@ -29,7 +29,7 @@ module AssignableTypes |
||
29 | 29 |
const_get(:TYPES).include?(type) |
30 | 30 |
end |
31 | 31 |
|
32 |
- def build_for_type(type, user, attributes) |
|
32 |
+ def build_for_type(type, user, attributes = {}) |
|
33 | 33 |
attributes.delete(:type) |
34 | 34 |
|
35 | 35 |
if valid_type?(type) |
@@ -0,0 +1,13 @@ |
||
1 |
+module HasGuid |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ |
|
4 |
+ included do |
|
5 |
+ before_save :make_guid |
|
6 |
+ end |
|
7 |
+ |
|
8 |
+ protected |
|
9 |
+ |
|
10 |
+ def make_guid |
|
11 |
+ self.guid = SecureRandom.hex unless guid.present? |
|
12 |
+ end |
|
13 |
+end |
@@ -21,12 +21,12 @@ class AgentsController < ApplicationController |
||
21 | 21 |
end |
22 | 22 |
|
23 | 23 |
def run |
24 |
- agent = current_user.agents.find(params[:id]) |
|
25 |
- Agent.async_check(agent.id) |
|
26 |
- if params[:return] == "show" |
|
27 |
- redirect_to agent_path(agent), notice: "Agent run queued" |
|
28 |
- else |
|
29 |
- redirect_to agents_path, notice: "Agent run queued" |
|
24 |
+ @agent = current_user.agents.find(params[:id]) |
|
25 |
+ Agent.async_check(@agent.id) |
|
26 |
+ |
|
27 |
+ respond_to do |format| |
|
28 |
+ format.html { redirect_back "Agent run queued for '#{@agent.name}'" } |
|
29 |
+ format.json { head :ok } |
|
30 | 30 |
end |
31 | 31 |
end |
32 | 32 |
|
@@ -53,12 +53,20 @@ class AgentsController < ApplicationController |
||
53 | 53 |
def remove_events |
54 | 54 |
@agent = current_user.agents.find(params[:id]) |
55 | 55 |
@agent.events.delete_all |
56 |
- redirect_to agents_path, notice: "All events removed" |
|
56 |
+ |
|
57 |
+ respond_to do |format| |
|
58 |
+ format.html { redirect_back "All emitted events removed for '#{@agent.name}'" } |
|
59 |
+ format.json { head :ok } |
|
60 |
+ end |
|
57 | 61 |
end |
58 | 62 |
|
59 | 63 |
def propagate |
60 |
- details = Agent.receive! |
|
61 |
- redirect_to agents_path, notice: "Queued propagation calls for #{details[:event_count]} event(s) on #{details[:agent_count]} agent(s)" |
|
64 |
+ details = Agent.receive! # Eventually this should probably be scoped to the current_user. |
|
65 |
+ |
|
66 |
+ respond_to do |format| |
|
67 |
+ format.html { redirect_back "Queued propagation calls for #{details[:event_count]} event(s) on #{details[:agent_count]} agent(s)" } |
|
68 |
+ format.json { head :ok } |
|
69 |
+ end |
|
62 | 70 |
end |
63 | 71 |
|
64 | 72 |
def show |
@@ -90,7 +98,11 @@ class AgentsController < ApplicationController |
||
90 | 98 |
end |
91 | 99 |
|
92 | 100 |
def diagram |
93 |
- @agents = current_user.agents.includes(:receivers) |
|
101 |
+ @agents = if params[:scenario_id].present? |
|
102 |
+ current_user.scenarios.find(params[:scenario_id]).agents.includes(:receivers) |
|
103 |
+ else |
|
104 |
+ current_user.agents.includes(:receivers) |
|
105 |
+ end |
|
94 | 106 |
end |
95 | 107 |
|
96 | 108 |
def create |
@@ -99,8 +111,8 @@ class AgentsController < ApplicationController |
||
99 | 111 |
params[:agent]) |
100 | 112 |
respond_to do |format| |
101 | 113 |
if @agent.save |
102 |
- format.html { redirect_to agents_path, notice: 'Your Agent was successfully created.' } |
|
103 |
- format.json { render json: @agent, status: :created, location: @agent } |
|
114 |
+ format.html { redirect_back "'#{@agent.name}' was successfully created." } |
|
115 |
+ format.json { render json: @agent, status: :ok, location: agent_path(@agent) } |
|
104 | 116 |
else |
105 | 117 |
format.html { render action: "new" } |
106 | 118 |
format.json { render json: @agent.errors, status: :unprocessable_entity } |
@@ -113,14 +125,8 @@ class AgentsController < ApplicationController |
||
113 | 125 |
|
114 | 126 |
respond_to do |format| |
115 | 127 |
if @agent.update_attributes(params[:agent]) |
116 |
- format.html { |
|
117 |
- if params[:return] == "show" |
|
118 |
- redirect_to agent_path(@agent), notice: 'Your Agent was successfully updated.' |
|
119 |
- else |
|
120 |
- redirect_to agents_path, notice: 'Your Agent was successfully updated.' |
|
121 |
- end |
|
122 |
- } |
|
123 |
- format.json { head :no_content } |
|
128 |
+ format.html { redirect_back "'#{@agent.name}' was successfully updated." } |
|
129 |
+ format.json { render json: @agent, status: :ok, location: agent_path(@agent) } |
|
124 | 130 |
else |
125 | 131 |
format.html { render action: "edit" } |
126 | 132 |
format.json { render json: @agent.errors, status: :unprocessable_entity } |
@@ -128,13 +134,39 @@ class AgentsController < ApplicationController |
||
128 | 134 |
end |
129 | 135 |
end |
130 | 136 |
|
137 |
+ def leave_scenario |
|
138 |
+ @agent = current_user.agents.find(params[:id]) |
|
139 |
+ @scenario = current_user.scenarios.find(params[:scenario_id]) |
|
140 |
+ @agent.scenarios.destroy(@scenario) |
|
141 |
+ |
|
142 |
+ respond_to do |format| |
|
143 |
+ format.html { redirect_back "'#{@agent.name}' removed from '#{@scenario.name}'" } |
|
144 |
+ format.json { head :no_content } |
|
145 |
+ end |
|
146 |
+ end |
|
147 |
+ |
|
131 | 148 |
def destroy |
132 | 149 |
@agent = current_user.agents.find(params[:id]) |
133 | 150 |
@agent.destroy |
134 | 151 |
|
135 | 152 |
respond_to do |format| |
136 |
- format.html { redirect_to agents_path } |
|
153 |
+ format.html { redirect_back "'#{@agent.name}' deleted" } |
|
137 | 154 |
format.json { head :no_content } |
138 | 155 |
end |
139 | 156 |
end |
157 |
+ |
|
158 |
+ protected |
|
159 |
+ |
|
160 |
+ # Sanitize params[:return] to prevent open redirect attacks, a common security issue. |
|
161 |
+ def redirect_back(message) |
|
162 |
+ if params[:return] == "show" && @agent |
|
163 |
+ path = agent_path(@agent) |
|
164 |
+ elsif params[:return] =~ /\A#{Regexp::escape scenarios_path}\/\d+\Z/ |
|
165 |
+ path = params[:return] |
|
166 |
+ else |
|
167 |
+ path = agents_path |
|
168 |
+ end |
|
169 |
+ |
|
170 |
+ redirect_to path, notice: message |
|
171 |
+ end |
|
140 | 172 |
end |
@@ -0,0 +1,20 @@ |
||
1 |
+class ScenarioImportsController < ApplicationController |
|
2 |
+ def new |
|
3 |
+ @scenario_import = ScenarioImport.new(:url => params[:url]) |
|
4 |
+ end |
|
5 |
+ |
|
6 |
+ def create |
|
7 |
+ @scenario_import = ScenarioImport.new(params[:scenario_import]) |
|
8 |
+ @scenario_import.set_user(current_user) |
|
9 |
+ |
|
10 |
+ if @scenario_import.will_request_local?(scenarios_url) |
|
11 |
+ render :text => 'Sorry, you cannot import a Scenario by URL from your own Huginn server.' and return |
|
12 |
+ end |
|
13 |
+ |
|
14 |
+ if @scenario_import.valid? && @scenario_import.should_import? && @scenario_import.import |
|
15 |
+ redirect_to @scenario_import.scenario, notice: "Import successful!" |
|
16 |
+ else |
|
17 |
+ render action: "new" |
|
18 |
+ end |
|
19 |
+ end |
|
20 |
+end |
@@ -0,0 +1,100 @@ |
||
1 |
+class ScenariosController < ApplicationController |
|
2 |
+ skip_before_filter :authenticate_user!, :only => :export |
|
3 |
+ |
|
4 |
+ def index |
|
5 |
+ @scenarios = current_user.scenarios.page(params[:page]) |
|
6 |
+ |
|
7 |
+ respond_to do |format| |
|
8 |
+ format.html |
|
9 |
+ format.json { render json: @scenarios } |
|
10 |
+ end |
|
11 |
+ end |
|
12 |
+ |
|
13 |
+ def new |
|
14 |
+ @scenario = current_user.scenarios.build |
|
15 |
+ |
|
16 |
+ respond_to do |format| |
|
17 |
+ format.html |
|
18 |
+ format.json { render json: @scenario } |
|
19 |
+ end |
|
20 |
+ end |
|
21 |
+ |
|
22 |
+ def show |
|
23 |
+ @scenario = current_user.scenarios.find(params[:id]) |
|
24 |
+ @agents = @scenario.agents.preload(:scenarios).page(params[:page]) |
|
25 |
+ |
|
26 |
+ respond_to do |format| |
|
27 |
+ format.html |
|
28 |
+ format.json { render json: @scenario } |
|
29 |
+ end |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ def share |
|
33 |
+ @scenario = current_user.scenarios.find(params[:id]) |
|
34 |
+ |
|
35 |
+ respond_to do |format| |
|
36 |
+ format.html |
|
37 |
+ format.json { render json: @scenario } |
|
38 |
+ end |
|
39 |
+ end |
|
40 |
+ |
|
41 |
+ def export |
|
42 |
+ @scenario = Scenario.find(params[:id]) |
|
43 |
+ raise ActiveRecord::RecordNotFound unless @scenario.public? || (current_user && current_user.id == @scenario.user_id) |
|
44 |
+ |
|
45 |
+ @exporter = AgentsExporter.new(:name => @scenario.name, |
|
46 |
+ :description => @scenario.description, |
|
47 |
+ :guid => @scenario.guid, |
|
48 |
+ :source_url => @scenario.public? && export_scenario_url(@scenario), |
|
49 |
+ :agents => @scenario.agents) |
|
50 |
+ response.headers['Content-Disposition'] = 'attachment; filename="' + @exporter.filename + '"' |
|
51 |
+ render :json => JSON.pretty_generate(@exporter.as_json) |
|
52 |
+ end |
|
53 |
+ |
|
54 |
+ def edit |
|
55 |
+ @scenario = current_user.scenarios.find(params[:id]) |
|
56 |
+ |
|
57 |
+ respond_to do |format| |
|
58 |
+ format.html |
|
59 |
+ format.json { render json: @scenario } |
|
60 |
+ end |
|
61 |
+ end |
|
62 |
+ |
|
63 |
+ def create |
|
64 |
+ @scenario = current_user.scenarios.build(params[:scenario]) |
|
65 |
+ |
|
66 |
+ respond_to do |format| |
|
67 |
+ if @scenario.save |
|
68 |
+ format.html { redirect_to @scenario, notice: 'This Scenario was successfully created.' } |
|
69 |
+ format.json { render json: @scenario, status: :created, location: @scenario } |
|
70 |
+ else |
|
71 |
+ format.html { render action: "new" } |
|
72 |
+ format.json { render json: @scenario.errors, status: :unprocessable_entity } |
|
73 |
+ end |
|
74 |
+ end |
|
75 |
+ end |
|
76 |
+ |
|
77 |
+ def update |
|
78 |
+ @scenario = current_user.scenarios.find(params[:id]) |
|
79 |
+ |
|
80 |
+ respond_to do |format| |
|
81 |
+ if @scenario.update_attributes(params[:scenario]) |
|
82 |
+ format.html { redirect_to @scenario, notice: 'This Scenario was successfully updated.' } |
|
83 |
+ format.json { head :no_content } |
|
84 |
+ else |
|
85 |
+ format.html { render action: "edit" } |
|
86 |
+ format.json { render json: @scenario.errors, status: :unprocessable_entity } |
|
87 |
+ end |
|
88 |
+ end |
|
89 |
+ end |
|
90 |
+ |
|
91 |
+ def destroy |
|
92 |
+ @scenario = current_user.scenarios.find(params[:id]) |
|
93 |
+ @scenario.destroy |
|
94 |
+ |
|
95 |
+ respond_to do |format| |
|
96 |
+ format.html { redirect_to scenarios_path } |
|
97 |
+ format.json { head :no_content } |
|
98 |
+ end |
|
99 |
+ end |
|
100 |
+end |
@@ -6,6 +6,12 @@ module AgentHelper |
||
6 | 6 |
end |
7 | 7 |
end |
8 | 8 |
|
9 |
+ def scenario_links(agent) |
|
10 |
+ agent.scenarios.map { |scenario| |
|
11 |
+ link_to(scenario.name, scenario, class: "label label-info") |
|
12 |
+ }.join(" ").html_safe |
|
13 |
+ end |
|
14 |
+ |
|
9 | 15 |
def agent_show_class(agent) |
10 | 16 |
agent.short_type.underscore.dasherize |
11 | 17 |
end |
@@ -35,6 +35,7 @@ module DotHelper |
||
35 | 35 |
dot << '%s;' % disabled_label(agent) |
36 | 36 |
end |
37 | 37 |
agent.receivers.each do |receiver| |
38 |
+ next unless agents.include?(receiver) |
|
38 | 39 |
dot << "%s->%s;" % [disabled_label(agent), disabled_label(receiver)] |
39 | 40 |
end |
40 | 41 |
end |
@@ -12,6 +12,7 @@ class Agent < ActiveRecord::Base |
||
12 | 12 |
include JSONSerializedField |
13 | 13 |
include RDBMSFunctions |
14 | 14 |
include WorkingHelpers |
15 |
+ include HasGuid |
|
15 | 16 |
|
16 | 17 |
markdown_class_attributes :description, :event_description |
17 | 18 |
|
@@ -22,13 +23,14 @@ class Agent < ActiveRecord::Base |
||
22 | 23 |
|
23 | 24 |
EVENT_RETENTION_SCHEDULES = [["Forever", 0], ["1 day", 1], *([2, 3, 4, 5, 7, 14, 21, 30, 45, 90, 180, 365].map {|n| ["#{n} days", n] })] |
24 | 25 |
|
25 |
- attr_accessible :options, :memory, :name, :type, :schedule, :disabled, :source_ids, :keep_events_for, :propagate_immediately |
|
26 |
+ attr_accessible :options, :memory, :name, :type, :schedule, :disabled, :source_ids, :scenario_ids, :keep_events_for, :propagate_immediately |
|
26 | 27 |
|
27 | 28 |
json_serialize :options, :memory |
28 | 29 |
|
29 | 30 |
validates_presence_of :name, :user |
30 | 31 |
validates_inclusion_of :keep_events_for, :in => EVENT_RETENTION_SCHEDULES.map(&:last) |
31 | 32 |
validate :sources_are_owned |
33 |
+ validate :scenarios_are_owned |
|
32 | 34 |
validate :validate_schedule |
33 | 35 |
validate :validate_options |
34 | 36 |
|
@@ -48,6 +50,8 @@ class Agent < ActiveRecord::Base |
||
48 | 50 |
has_many :links_as_receiver, :dependent => :delete_all, :foreign_key => "receiver_id", :class_name => "Link", :inverse_of => :receiver |
49 | 51 |
has_many :sources, :through => :links_as_receiver, :class_name => "Agent", :inverse_of => :receivers |
50 | 52 |
has_many :receivers, :through => :links_as_source, :class_name => "Agent", :inverse_of => :sources |
53 |
+ has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :agent |
|
54 |
+ has_many :scenarios, :through => :scenario_memberships, :inverse_of => :agents |
|
51 | 55 |
|
52 | 56 |
scope :of_type, lambda { |type| |
53 | 57 |
type = case type |
@@ -206,6 +210,10 @@ class Agent < ActiveRecord::Base |
||
206 | 210 |
errors.add(:sources, "must be owned by you") unless sources.all? {|s| s.user == user } |
207 | 211 |
end |
208 | 212 |
|
213 |
+ def scenarios_are_owned |
|
214 |
+ errors.add(:scenarios, "must be owned by you") unless scenarios.all? {|s| s.user == user } |
|
215 |
+ end |
|
216 |
+ |
|
209 | 217 |
def validate_schedule |
210 | 218 |
unless cannot_be_scheduled? |
211 | 219 |
errors.add(:schedule, "is not a valid schedule") unless SCHEDULES.include?(schedule.to_s) |
@@ -19,13 +19,13 @@ module Agents |
||
19 | 19 |
Your event can provide any of the following optional parameters or you can provide defaults: |
20 | 20 |
|
21 | 21 |
* `device` - your user's device name to send the message directly to that device, rather than all of the user's devices |
22 |
- * `title` or `subject` - your notifications's title |
|
22 |
+ * `title` or `subject` - your notification's title |
|
23 | 23 |
* `url` - a supplementary URL to show with your message - `512` Character Limit |
24 | 24 |
* `url_title` - a title for your supplementary URL, otherwise just the URL is shown - `100` Character Limit |
25 | 25 |
* `priority` - send as `-1` to always send as a quiet notification, `0` is default, `1` to display as high-priority and bypass the user's quiet hours, or `2` for emergency priority: [Please read Pushover Docs on Emergency Priority](https://pushover.net/api#priority) |
26 | 26 |
* `sound` - the name of one of the sounds supported by device clients to override the user's default sound choice. [See PushOver docs for sound options.](https://pushover.net/api#sounds) |
27 |
- * `retry` - Requred for emergency priority - Specifies how often (in seconds) the Pushover servers will send the same notification to the user. Minimum value: `30` |
|
28 |
- * `expire` - Requred for emergency priority - Specifies how many seconds your notification will continue to be retried for (every retry seconds). Maximum value: `86400` |
|
27 |
+ * `retry` - Required for emergency priority - Specifies how often (in seconds) the Pushover servers will send the same notification to the user. Minimum value: `30` |
|
28 |
+ * `expire` - Required for emergency priority - Specifies how many seconds your notification will continue to be retried for (every retry seconds). Maximum value: `86400` |
|
29 | 29 |
|
30 | 30 |
Your event can also pass along a timestamp parameter: |
31 | 31 |
|
@@ -42,10 +42,10 @@ module Agents |
||
42 | 42 |
'title' => '', |
43 | 43 |
'url' => '', |
44 | 44 |
'url_title' => '', |
45 |
- 'priority' => 0, |
|
45 |
+ 'priority' => '0', |
|
46 | 46 |
'sound' => 'pushover', |
47 |
- 'retry' => 0, |
|
48 |
- 'expire' => 0, |
|
47 |
+ 'retry' => '0', |
|
48 |
+ 'expire' => '0', |
|
49 | 49 |
'expected_receive_period_in_days' => '1' |
50 | 50 |
} |
51 | 51 |
end |
@@ -102,6 +102,5 @@ module Agents |
||
102 | 102 |
response = HTTParty.post(API_URL, :query => post_params) |
103 | 103 |
puts response |
104 | 104 |
end |
105 |
- |
|
106 | 105 |
end |
107 | 106 |
end |
@@ -0,0 +1,19 @@ |
||
1 |
+class Scenario < ActiveRecord::Base |
|
2 |
+ include HasGuid |
|
3 |
+ |
|
4 |
+ attr_accessible :name, :agent_ids, :description, :public, :source_url |
|
5 |
+ |
|
6 |
+ belongs_to :user, :counter_cache => :scenario_count, :inverse_of => :scenarios |
|
7 |
+ has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :scenario |
|
8 |
+ has_many :agents, :through => :scenario_memberships, :inverse_of => :scenarios |
|
9 |
+ |
|
10 |
+ validates_presence_of :name, :user |
|
11 |
+ |
|
12 |
+ validate :agents_are_owned |
|
13 |
+ |
|
14 |
+ protected |
|
15 |
+ |
|
16 |
+ def agents_are_owned |
|
17 |
+ errors.add(:agents, "must be owned by you") unless agents.all? {|s| s.user == user } |
|
18 |
+ end |
|
19 |
+end |
@@ -0,0 +1,256 @@ |
||
1 |
+require 'ostruct' |
|
2 |
+ |
|
3 |
+# This is a helper class for managing Scenario imports, used by the ScenarioImportsController. This class behaves much |
|
4 |
+# like a normal ActiveRecord object, with validations and callbacks. However, it is never persisted to the database. |
|
5 |
+class ScenarioImport |
|
6 |
+ include ActiveModel::Model |
|
7 |
+ include ActiveModel::Callbacks |
|
8 |
+ include ActiveModel::Validations::Callbacks |
|
9 |
+ |
|
10 |
+ DANGEROUS_AGENT_TYPES = %w[Agents::ShellCommandAgent] |
|
11 |
+ URL_REGEX = /\Ahttps?:\/\//i |
|
12 |
+ |
|
13 |
+ attr_accessor :file, :url, :data, :do_import, :merges |
|
14 |
+ |
|
15 |
+ attr_reader :user |
|
16 |
+ |
|
17 |
+ before_validation :parse_file |
|
18 |
+ before_validation :fetch_url |
|
19 |
+ |
|
20 |
+ validate :validate_presence_of_file_url_or_data |
|
21 |
+ validates_format_of :url, :with => URL_REGEX, :allow_nil => true, :allow_blank => true, :message => "appears to be invalid" |
|
22 |
+ validate :validate_data |
|
23 |
+ validate :generate_diff |
|
24 |
+ |
|
25 |
+ def step_one? |
|
26 |
+ data.blank? |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ def step_two? |
|
30 |
+ data.present? |
|
31 |
+ end |
|
32 |
+ |
|
33 |
+ def set_user(user) |
|
34 |
+ @user = user |
|
35 |
+ end |
|
36 |
+ |
|
37 |
+ def existing_scenario |
|
38 |
+ @existing_scenario ||= user.scenarios.find_by(:guid => parsed_data["guid"]) |
|
39 |
+ end |
|
40 |
+ |
|
41 |
+ def dangerous? |
|
42 |
+ (parsed_data['agents'] || []).any? { |agent| DANGEROUS_AGENT_TYPES.include?(agent['type']) } |
|
43 |
+ end |
|
44 |
+ |
|
45 |
+ def parsed_data |
|
46 |
+ @parsed_data ||= (data && JSON.parse(data) rescue {}) || {} |
|
47 |
+ end |
|
48 |
+ |
|
49 |
+ def agent_diffs |
|
50 |
+ @agent_diffs || generate_diff |
|
51 |
+ end |
|
52 |
+ |
|
53 |
+ def should_import? |
|
54 |
+ do_import == "1" |
|
55 |
+ end |
|
56 |
+ |
|
57 |
+ def import(options = {}) |
|
58 |
+ success = true |
|
59 |
+ guid = parsed_data['guid'] |
|
60 |
+ description = parsed_data['description'] |
|
61 |
+ name = parsed_data['name'] |
|
62 |
+ links = parsed_data['links'] |
|
63 |
+ source_url = parsed_data['source_url'].presence || nil |
|
64 |
+ @scenario = user.scenarios.where(:guid => guid).first_or_initialize |
|
65 |
+ @scenario.update_attributes!(:name => name, :description => description, |
|
66 |
+ :source_url => source_url, :public => false) |
|
67 |
+ |
|
68 |
+ unless options[:skip_agents] |
|
69 |
+ created_agents = agent_diffs.map do |agent_diff| |
|
70 |
+ agent = agent_diff.agent || Agent.build_for_type("Agents::" + agent_diff.type.incoming, user) |
|
71 |
+ agent.guid = agent_diff.guid.incoming |
|
72 |
+ agent.attributes = { :name => agent_diff.name.updated, |
|
73 |
+ :disabled => agent_diff.disabled.updated, # == "true" |
|
74 |
+ :options => agent_diff.options.updated, |
|
75 |
+ :scenario_ids => [@scenario.id] } |
|
76 |
+ agent.schedule = agent_diff.schedule.updated if agent_diff.schedule.present? |
|
77 |
+ agent.keep_events_for = agent_diff.keep_events_for.updated if agent_diff.keep_events_for.present? |
|
78 |
+ agent.propagate_immediately = agent_diff.propagate_immediately.updated if agent_diff.propagate_immediately.present? # == "true" |
|
79 |
+ unless agent.save |
|
80 |
+ success = false |
|
81 |
+ errors.add(:base, "Errors when saving '#{agent_diff.name.incoming}': #{agent.errors.full_messages.to_sentence}") |
|
82 |
+ end |
|
83 |
+ agent |
|
84 |
+ end |
|
85 |
+ |
|
86 |
+ links.each do |link| |
|
87 |
+ receiver = created_agents[link['receiver']] |
|
88 |
+ source = created_agents[link['source']] |
|
89 |
+ receiver.sources << source unless receiver.sources.include?(source) |
|
90 |
+ end |
|
91 |
+ end |
|
92 |
+ |
|
93 |
+ success |
|
94 |
+ end |
|
95 |
+ |
|
96 |
+ def scenario |
|
97 |
+ @scenario || @existing_scenario |
|
98 |
+ end |
|
99 |
+ |
|
100 |
+ def will_request_local?(url_root) |
|
101 |
+ data.blank? && file.blank? && url.present? && url.starts_with?(url_root) |
|
102 |
+ end |
|
103 |
+ |
|
104 |
+ protected |
|
105 |
+ |
|
106 |
+ def parse_file |
|
107 |
+ if data.blank? && file.present? |
|
108 |
+ self.data = file.read |
|
109 |
+ end |
|
110 |
+ end |
|
111 |
+ |
|
112 |
+ def fetch_url |
|
113 |
+ if data.blank? && url.present? && url =~ URL_REGEX |
|
114 |
+ self.data = Faraday.get(url).body |
|
115 |
+ end |
|
116 |
+ end |
|
117 |
+ |
|
118 |
+ def validate_data |
|
119 |
+ if data.present? |
|
120 |
+ @parsed_data = JSON.parse(data) rescue {} |
|
121 |
+ if (%w[name guid agents] - @parsed_data.keys).length > 0 |
|
122 |
+ errors.add(:base, "The provided data does not appear to be a valid Scenario.") |
|
123 |
+ self.data = nil |
|
124 |
+ end |
|
125 |
+ else |
|
126 |
+ @parsed_data = nil |
|
127 |
+ end |
|
128 |
+ end |
|
129 |
+ |
|
130 |
+ def validate_presence_of_file_url_or_data |
|
131 |
+ unless file.present? || url.present? || data.present? |
|
132 |
+ errors.add(:base, "Please provide either a Scenario JSON File or a Public Scenario URL.") |
|
133 |
+ end |
|
134 |
+ end |
|
135 |
+ |
|
136 |
+ def generate_diff |
|
137 |
+ @agent_diffs = (parsed_data['agents'] || []).map.with_index do |agent_data, index| |
|
138 |
+ # AgentDiff is defined at the end of this file. |
|
139 |
+ agent_diff = AgentDiff.new(agent_data) |
|
140 |
+ if existing_scenario |
|
141 |
+ # If this Agent exists already, update the AgentDiff with the local version's information. |
|
142 |
+ agent_diff.diff_with! existing_scenario.agents.find_by(:guid => agent_data['guid']) |
|
143 |
+ |
|
144 |
+ begin |
|
145 |
+ # Update the AgentDiff with any hand-merged changes coming from the UI. This only happens when this |
|
146 |
+ # Agent already exists locally and has conflicting changes. |
|
147 |
+ agent_diff.update_from! merges[index.to_s] if merges |
|
148 |
+ rescue JSON::ParserError |
|
149 |
+ errors.add(:base, "Your updated options for '#{agent_data['name']}' were unparsable.") |
|
150 |
+ end |
|
151 |
+ end |
|
152 |
+ agent_diff |
|
153 |
+ end |
|
154 |
+ end |
|
155 |
+ |
|
156 |
+ # AgentDiff is a helper object that encapsulates an incoming Agent. All fields will be returned as an array |
|
157 |
+ # of either one or two values. The first value is the incoming value, the second is the existing value, if |
|
158 |
+ # it differs from the incoming value. |
|
159 |
+ class AgentDiff < OpenStruct |
|
160 |
+ class FieldDiff |
|
161 |
+ attr_accessor :incoming, :current, :updated |
|
162 |
+ |
|
163 |
+ def initialize(incoming) |
|
164 |
+ @incoming = incoming |
|
165 |
+ @updated = incoming |
|
166 |
+ end |
|
167 |
+ |
|
168 |
+ def set_current(current) |
|
169 |
+ @current = current |
|
170 |
+ @requires_merge = (incoming != current) |
|
171 |
+ end |
|
172 |
+ |
|
173 |
+ def requires_merge? |
|
174 |
+ @requires_merge |
|
175 |
+ end |
|
176 |
+ end |
|
177 |
+ |
|
178 |
+ def initialize(agent_data) |
|
179 |
+ super() |
|
180 |
+ @requires_merge = false |
|
181 |
+ self.agent = nil |
|
182 |
+ store! agent_data |
|
183 |
+ end |
|
184 |
+ |
|
185 |
+ BASE_FIELDS = %w[name schedule keep_events_for propagate_immediately disabled guid] |
|
186 |
+ |
|
187 |
+ def agent_exists? |
|
188 |
+ !!agent |
|
189 |
+ end |
|
190 |
+ |
|
191 |
+ def requires_merge? |
|
192 |
+ @requires_merge |
|
193 |
+ end |
|
194 |
+ |
|
195 |
+ def store!(agent_data) |
|
196 |
+ self.type = FieldDiff.new(agent_data["type"].split("::").pop) |
|
197 |
+ self.options = FieldDiff.new(agent_data['options'] || {}) |
|
198 |
+ BASE_FIELDS.each do |option| |
|
199 |
+ self[option] = FieldDiff.new(agent_data[option]) if agent_data.has_key?(option) |
|
200 |
+ end |
|
201 |
+ end |
|
202 |
+ |
|
203 |
+ def diff_with!(agent) |
|
204 |
+ return unless agent.present? |
|
205 |
+ |
|
206 |
+ self.agent = agent |
|
207 |
+ |
|
208 |
+ type.set_current(agent.short_type) |
|
209 |
+ options.set_current(agent.options || {}) |
|
210 |
+ |
|
211 |
+ @requires_merge ||= type.requires_merge? |
|
212 |
+ @requires_merge ||= options.requires_merge? |
|
213 |
+ |
|
214 |
+ BASE_FIELDS.each do |field| |
|
215 |
+ next unless self[field].present? |
|
216 |
+ self[field].set_current(agent.send(field)) |
|
217 |
+ |
|
218 |
+ @requires_merge ||= self[field].requires_merge? |
|
219 |
+ end |
|
220 |
+ end |
|
221 |
+ |
|
222 |
+ def update_from!(merges) |
|
223 |
+ each_field do |field, value, selection_options| |
|
224 |
+ value.updated = merges[field] |
|
225 |
+ end |
|
226 |
+ |
|
227 |
+ if options.requires_merge? |
|
228 |
+ options.updated = JSON.parse(merges['options']) |
|
229 |
+ end |
|
230 |
+ end |
|
231 |
+ |
|
232 |
+ def each_field |
|
233 |
+ boolean = [["True", "true"], ["False", "false"]] |
|
234 |
+ yield 'name', name if name.requires_merge? |
|
235 |
+ yield 'schedule', schedule, Agent::SCHEDULES.map {|s| [s.humanize.titleize, s] } if self['schedule'].present? && schedule.requires_merge? |
|
236 |
+ yield 'keep_events_for', keep_events_for, Agent::EVENT_RETENTION_SCHEDULES if self['keep_events_for'].present? && keep_events_for.requires_merge? |
|
237 |
+ yield 'propagate_immediately', propagate_immediately, boolean if self['propagate_immediately'].present? && propagate_immediately.requires_merge? |
|
238 |
+ yield 'disabled', disabled, boolean if disabled.requires_merge? |
|
239 |
+ end |
|
240 |
+ |
|
241 |
+ # Unfortunately Ruby 1.9's OpenStruct doesn't expose [] and []=. |
|
242 |
+ unless instance_methods.include?(:[]=) |
|
243 |
+ def [](key) |
|
244 |
+ self.send(sanitize key) |
|
245 |
+ end |
|
246 |
+ |
|
247 |
+ def []=(key, val) |
|
248 |
+ self.send("#{sanitize key}=", val) |
|
249 |
+ end |
|
250 |
+ |
|
251 |
+ def sanitize(key) |
|
252 |
+ key.gsub(/[^a-zA-Z0-9_-]/, '') |
|
253 |
+ end |
|
254 |
+ end |
|
255 |
+ end |
|
256 |
+end |
@@ -0,0 +1,4 @@ |
||
1 |
+class ScenarioMembership < ActiveRecord::Base |
|
2 |
+ belongs_to :agent, :inverse_of => :scenario_memberships |
|
3 |
+ belongs_to :scenario, :inverse_of => :scenario_memberships |
|
4 |
+end |
@@ -26,6 +26,7 @@ class User < ActiveRecord::Base |
||
26 | 26 |
has_many :events, -> { order("events.created_at desc") }, :dependent => :delete_all, :inverse_of => :user |
27 | 27 |
has_many :agents, -> { order("agents.created_at desc") }, :dependent => :destroy, :inverse_of => :user |
28 | 28 |
has_many :logs, :through => :agents, :class_name => "AgentLog" |
29 |
+ has_many :scenarios, :inverse_of => :user, :dependent => :destroy |
|
29 | 30 |
|
30 | 31 |
# Allow users to login via either email or username. |
31 | 32 |
def self.find_first_by_auth_conditions(warden_conditions) |
@@ -27,15 +27,25 @@ |
||
27 | 27 |
<% end %> |
28 | 28 |
</li> |
29 | 29 |
|
30 |
+ <% if agent.scenarios.length > 0 %> |
|
31 |
+ <li class="divider"></li> |
|
32 |
+ |
|
33 |
+ <% agent.scenarios.each do |scenario| %> |
|
34 |
+ <li> |
|
35 |
+ <%= link_to "<span class='color-warning glyphicon glyphicon-remove-circle'></span> Remove from <span class='scenario label label-info'>#{h scenario.name}</span>".html_safe, leave_scenario_agent_path(agent, :scenario_id => scenario.to_param, :return => returnTo), method: :put, :tabindex => "-1" %> |
|
36 |
+ </li> |
|
37 |
+ <% end %> |
|
38 |
+ <% end %> |
|
39 |
+ |
|
30 | 40 |
<li class="divider"></li> |
31 | 41 |
|
32 | 42 |
<% if agent.can_create_events? && agent.events.count > 0 %> |
33 | 43 |
<li> |
34 |
- <%= link_to '<span class="color-danger glyphicon glyphicon-trash"></span> Delete all events'.html_safe, remove_events_agent_path(agent), method: :delete, data: {confirm: 'Are you sure you want to delete ALL events for this Agent?'}, :tabindex => "-1" %> |
|
44 |
+ <%= link_to '<span class="color-danger glyphicon glyphicon-trash"></span> Delete all events'.html_safe, remove_events_agent_path(agent, :return => returnTo), method: :delete, data: {confirm: 'Are you sure you want to delete ALL emitted events for this Agent?'}, :tabindex => "-1" %> |
|
35 | 45 |
</li> |
36 | 46 |
<% end %> |
37 | 47 |
|
38 | 48 |
<li> |
39 |
- <%= link_to '<span class="color-danger glyphicon glyphicon-remove"></span> Delete agent'.html_safe, agent_path(agent), method: :delete, data: { confirm: 'Are you sure?' }, :tabindex => "-1" %> |
|
49 |
+ <%= link_to '<span class="color-danger glyphicon glyphicon-remove"></span> Delete agent'.html_safe, agent_path(agent, :return => returnTo), method: :delete, data: { confirm: 'Are you sure that you want to permanently delete this Agent?' }, :tabindex => "-1" %> |
|
40 | 50 |
</li> |
41 | 51 |
</ul> |
@@ -41,6 +41,7 @@ |
||
41 | 41 |
<div class='event-related-region' data-can-create-events="<%= @agent.can_create_events? %>"> |
42 | 42 |
<div class="form-group"> |
43 | 43 |
<%= f.label :keep_events_for, "Keep events" %> |
44 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="In order to conserve disk space, you can choose to have events created by this Agent expire after a certain period of time. Make sure you keep them long enough to allow any subsequent Agents to make use of them."></span> |
|
44 | 45 |
<%= f.select :keep_events_for, options_for_select(Agent::EVENT_RETENTION_SCHEDULES, @agent.keep_events_for), {}, :class => 'form-control' %> |
45 | 46 |
</div> |
46 | 47 |
</div> |
@@ -59,13 +60,24 @@ |
||
59 | 60 |
<% end %> |
60 | 61 |
</div> |
61 | 62 |
</div> |
63 |
+ |
|
64 |
+ <% if current_user.scenario_count > 0 %> |
|
65 |
+ <div class="form-group"> |
|
66 |
+ <%= f.label :scenarios %> |
|
67 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="Use Scenarios to group sets of Agents, both for organization, and to make them easy to export and share."></span> |
|
68 |
+ <%= f.select(:scenario_ids, |
|
69 |
+ options_for_select(current_user.scenarios.pluck(:name, :id), @agent.scenario_ids), |
|
70 |
+ {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %> |
|
71 |
+ </div> |
|
72 |
+ <% end %> |
|
73 |
+ |
|
62 | 74 |
</div> |
63 | 75 |
|
64 | 76 |
<!-- Form controls full width --> |
65 | 77 |
<div class="col-md-12"> |
66 | 78 |
<div class="form-group"> |
67 | 79 |
<%= f.label :options %> |
68 |
- <textarea rows="10" id="agent_options" name="agent[options]" class="form-control live-json-editor <%= (@agent.new_record? && @agent.options == {}) ? "showing-default" : "" %>"> |
|
80 |
+ <textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor <%= (@agent.new_record? && @agent.options == {}) ? "showing-default" : "" %>"> |
|
69 | 81 |
<%= Utils.jsonify((@agent.new_record? && @agent.options == {}) ? @agent.default_options : @agent.options) %> |
70 | 82 |
</textarea> |
71 | 83 |
</div> |
@@ -92,7 +104,7 @@ |
||
92 | 104 |
|
93 | 105 |
<div class='row'> |
94 | 106 |
<div class="col-md-12"> |
95 |
- <%= f.submit :class => "btn btn-primary" %> |
|
107 |
+ <%= f.submit "Save", :class => "btn btn-primary" %> |
|
96 | 108 |
</div> |
97 | 109 |
</div> |
98 | 110 |
|
@@ -0,0 +1,75 @@ |
||
1 |
+<div class='table-responsive'> |
|
2 |
+ <table class='table table-striped'> |
|
3 |
+ <tr> |
|
4 |
+ <th>Name</th> |
|
5 |
+ <th>Schedule</th> |
|
6 |
+ <th>Last Check</th> |
|
7 |
+ <th>Last Event Out</th> |
|
8 |
+ <th>Last Event In</th> |
|
9 |
+ <th>Events Created</th> |
|
10 |
+ <th>Working?</th> |
|
11 |
+ <th></th> |
|
12 |
+ </tr> |
|
13 |
+ |
|
14 |
+ <% @agents.each do |agent| %> |
|
15 |
+ <tr> |
|
16 |
+ <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
17 |
+ <%= link_to agent.name, agent_path(agent) %> |
|
18 |
+ <br/> |
|
19 |
+ <span class='text-muted'><%= agent.short_type.titleize %></span> |
|
20 |
+ <% if agent.scenarios.present? %> |
|
21 |
+ <span> |
|
22 |
+ <%= scenario_links(agent) %> |
|
23 |
+ </span> |
|
24 |
+ <% end %> |
|
25 |
+ </td> |
|
26 |
+ <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
27 |
+ <% if agent.can_be_scheduled? %> |
|
28 |
+ <%= agent.schedule.to_s.humanize.titleize %> |
|
29 |
+ <% else %> |
|
30 |
+ <span class='not-applicable'></span> |
|
31 |
+ <% end %> |
|
32 |
+ </td> |
|
33 |
+ <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
34 |
+ <% if agent.can_be_scheduled? %> |
|
35 |
+ <%= agent.last_check_at ? time_ago_in_words(agent.last_check_at) + " ago" : "never" %> |
|
36 |
+ <% else %> |
|
37 |
+ <span class='not-applicable'></span> |
|
38 |
+ <% end %> |
|
39 |
+ </td> |
|
40 |
+ <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
41 |
+ <% if agent.can_create_events? %> |
|
42 |
+ <%= agent.last_event_at ? time_ago_in_words(agent.last_event_at) + " ago" : "never" %> |
|
43 |
+ <% else %> |
|
44 |
+ <span class='not-applicable'></span> |
|
45 |
+ <% end %> |
|
46 |
+ </td> |
|
47 |
+ <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
48 |
+ <% if agent.can_receive_events? %> |
|
49 |
+ <%= agent.last_receive_at ? time_ago_in_words(agent.last_receive_at) + " ago" : "never" %> |
|
50 |
+ <% else %> |
|
51 |
+ <span class='not-applicable'></span> |
|
52 |
+ <% end %> |
|
53 |
+ </td> |
|
54 |
+ <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
55 |
+ <% if agent.can_create_events? %> |
|
56 |
+ <%= link_to(agent.events_count || 0, events_path(:agent => agent.to_param)) %> |
|
57 |
+ <% else %> |
|
58 |
+ <span class='not-applicable'></span> |
|
59 |
+ <% end %> |
|
60 |
+ </td> |
|
61 |
+ <td><%= working(agent) %></td> |
|
62 |
+ <td> |
|
63 |
+ <div class="btn-group"> |
|
64 |
+ <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown"> |
|
65 |
+ <span class="glyphicon glyphicon-th-list"></span> Actions <span class="caret"></span> |
|
66 |
+ </button> |
|
67 |
+ <%= render 'agents/action_menu', :agent => agent, :returnTo => (defined?(returnTo) && returnTo) || "index" %> |
|
68 |
+ </div> |
|
69 |
+ </td> |
|
70 |
+ </tr> |
|
71 |
+ <% end %> |
|
72 |
+ </table> |
|
73 |
+</div> |
|
74 |
+ |
|
75 |
+<%= paginate @agents, :theme => 'twitter-bootstrap-3' %> |
@@ -14,7 +14,7 @@ |
||
14 | 14 |
|
15 | 15 |
<script> |
16 | 16 |
$(function () { |
17 |
- var payloadJsonEditor = window.setupJsonEditor($(".payload-editor")); |
|
17 |
+ var payloadJsonEditor = window.setupJsonEditor($(".payload-editor"))[0]; |
|
18 | 18 |
$("#create-event-form").submit(function (e) { |
19 | 19 |
e.preventDefault(); |
20 | 20 |
var $form = $("#create-event-form"); |
@@ -5,7 +5,7 @@ |
||
5 | 5 |
<h2>Agent Event Flow</h2> |
6 | 6 |
</div> |
7 | 7 |
<div class="btn-group"> |
8 |
- <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, agents_path, class: "btn btn-default" %> |
|
8 |
+ <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, (params[:scenario_id] ? scenario_path(params[:scenario_id]) : agents_path), class: "btn btn-default" %> |
|
9 | 9 |
</div> |
10 | 10 |
|
11 | 11 |
<div class='digraph'> |
@@ -5,76 +5,7 @@ |
||
5 | 5 |
<h2>Your Agents</h2> |
6 | 6 |
</div> |
7 | 7 |
|
8 |
- <div class='table-responsive'> |
|
9 |
- <table class='table table-striped'> |
|
10 |
- <tr> |
|
11 |
- <th>Name</th> |
|
12 |
- <th>Schedule</th> |
|
13 |
- <th>Last Check</th> |
|
14 |
- <th>Last Event Out</th> |
|
15 |
- <th>Last Event In</th> |
|
16 |
- <th>Events Created</th> |
|
17 |
- <th>Working?</th> |
|
18 |
- <th></th> |
|
19 |
- </tr> |
|
20 |
- |
|
21 |
- <% @agents.each do |agent| %> |
|
22 |
- <tr> |
|
23 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
24 |
- <%= link_to agent.name, agent_path(agent) %> |
|
25 |
- <br/> |
|
26 |
- <span class='text-muted'><%= agent.short_type.titleize %></span> |
|
27 |
- </td> |
|
28 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
29 |
- <% if agent.can_be_scheduled? %> |
|
30 |
- <%= agent.schedule.to_s.humanize.titleize %> |
|
31 |
- <% else %> |
|
32 |
- <span class='not-applicable'></span> |
|
33 |
- <% end %> |
|
34 |
- </td> |
|
35 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
36 |
- <% if agent.can_be_scheduled? %> |
|
37 |
- <%= agent.last_check_at ? time_ago_in_words(agent.last_check_at) + " ago" : "never" %> |
|
38 |
- <% else %> |
|
39 |
- <span class='not-applicable'></span> |
|
40 |
- <% end %> |
|
41 |
- </td> |
|
42 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
43 |
- <% if agent.can_create_events? %> |
|
44 |
- <%= agent.last_event_at ? time_ago_in_words(agent.last_event_at) + " ago" : "never" %> |
|
45 |
- <% else %> |
|
46 |
- <span class='not-applicable'></span> |
|
47 |
- <% end %> |
|
48 |
- </td> |
|
49 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
50 |
- <% if agent.can_receive_events? %> |
|
51 |
- <%= agent.last_receive_at ? time_ago_in_words(agent.last_receive_at) + " ago" : "never" %> |
|
52 |
- <% else %> |
|
53 |
- <span class='not-applicable'></span> |
|
54 |
- <% end %> |
|
55 |
- </td> |
|
56 |
- <td class='<%= "agent-disabled" if agent.disabled? %>'> |
|
57 |
- <% if agent.can_create_events? %> |
|
58 |
- <%= link_to(agent.events_count || 0, events_path(:agent => agent.to_param)) %> |
|
59 |
- <% else %> |
|
60 |
- <span class='not-applicable'></span> |
|
61 |
- <% end %> |
|
62 |
- </td> |
|
63 |
- <td><%= working(agent) %></td> |
|
64 |
- <td> |
|
65 |
- <div class="btn-group"> |
|
66 |
- <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown"> |
|
67 |
- <span class="glyphicon glyphicon-th-list"></span> Actions <span class="caret"></span> |
|
68 |
- </button> |
|
69 |
- <%= render 'action_menu', :agent => agent, :returnTo => "index" %> |
|
70 |
- </div> |
|
71 |
- </td> |
|
72 |
- </tr> |
|
73 |
- <% end %> |
|
74 |
- </table> |
|
75 |
- </div> |
|
76 |
- |
|
77 |
- <%= paginate @agents, :theme => 'twitter-bootstrap-3' %> |
|
8 |
+ <%= render 'agents/table' %> |
|
78 | 9 |
|
79 | 10 |
<br/> |
80 | 11 |
|
@@ -22,7 +22,7 @@ |
||
22 | 22 |
|
23 | 23 |
<li class="dropdown"> |
24 | 24 |
<a class="dropdown-toggle" data-toggle="dropdown" href="#"><span class="glyphicon glyphicon-th-list"></span> Actions <span class="caret"></span></a> |
25 |
- <%= render 'action_menu', :agent => @agent, :returnTo => "show" %> |
|
25 |
+ <%= render 'agents/action_menu', :agent => @agent, :returnTo => "show" %> |
|
26 | 26 |
</li> |
27 | 27 |
</ul> |
28 | 28 |
</div> |
@@ -20,13 +20,13 @@ |
||
20 | 20 |
<% next unless event.agent %> |
21 | 21 |
<tr> |
22 | 22 |
<td><%= link_to event.agent.name, agent_path(event.agent) %></td> |
23 |
- <td><%= time_ago_in_words event.created_at %> ago</td> |
|
23 |
+ <td title='<%= event.created_at %>'><%= time_ago_in_words event.created_at %> ago</td> |
|
24 | 24 |
<td class='payload'><%= truncate event.payload.to_json, :length => 90, :omission => "" %></td> |
25 | 25 |
<td> |
26 | 26 |
<div class="btn-group btn-group-xs"> |
27 | 27 |
<%= link_to 'Show', event_path(event), class: "btn btn-default" %> |
28 | 28 |
<%= link_to 'Re-emit', reemit_event_path(event), method: :post, data: { confirm: 'Are you sure you want to duplicate this event and emit the new one now?' }, class: "btn btn-default" %> |
29 |
- <%= link_to 'Delete', event_path(event), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-default btn-danger" %> |
|
29 |
+ <%= link_to 'Delete', event_path(event), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-default" %> |
|
30 | 30 |
</div> |
31 | 31 |
</td> |
32 | 32 |
</tr> |
@@ -13,6 +13,7 @@ |
||
13 | 13 |
<% if user_signed_in? %> |
14 | 14 |
<ul class='nav navbar-nav'> |
15 | 15 |
<%= nav_link "Agents", agents_path %> |
16 |
+ <%= nav_link "Scenarios", scenarios_path %> |
|
16 | 17 |
<%= nav_link "Events", events_path %> |
17 | 18 |
<%= nav_link "Credentials", user_credentials_path %> |
18 | 19 |
</ul> |
@@ -31,18 +31,22 @@ |
||
31 | 31 |
|
32 | 32 |
<script> |
33 | 33 |
var agentPaths = {}; |
34 |
- <% if current_user -%> |
|
35 |
- var myAgents = <%= Utils.jsonify(current_user.agents.select([:name, :id, :schedule]).inject({}) {|m, a| m[a.name] = agent_path(a); m }) %>; |
|
34 |
+ var agentNames = []; |
|
35 |
+ <% if current_user.present? -%> |
|
36 |
+ var myAgents = <%= Utils.jsonify(current_user.agents.pluck(:name, :id).inject({}) {|m, a| m[a.first] = agent_path(a.last); m }) %>; |
|
37 |
+ var myScenarios = <%= Utils.jsonify(current_user.scenarios.pluck(:name, :id).inject({}) {|m, s| m[s.first + " Scenario"] = scenario_path(s.last); m }) %>; |
|
36 | 38 |
$.extend(agentPaths, myAgents); |
39 |
+ $.extend(agentPaths, myScenarios); |
|
40 |
+ agentPaths["All Agents Index"] = <%= Utils.jsonify agents_path %>; |
|
41 |
+ agentPaths["New Agent"] = <%= Utils.jsonify new_agent_path %>; |
|
42 |
+ agentPaths["Account"] = <%= Utils.jsonify edit_user_registration_path %>; |
|
43 |
+ agentPaths["Events Index"] = <%= Utils.jsonify events_path %>; |
|
44 |
+ agentPaths["View Agent Diagram"] = <%= Utils.jsonify diagram_agents_path %>; |
|
45 |
+ agentPaths["Run Event Propagation"] = { url: <%= Utils.jsonify propagate_agents_path %>, method: 'POST' }; |
|
46 |
+ |
|
47 |
+ |
|
48 |
+ $.each(agentPaths, function(name, v) { agentNames.push(name); }); |
|
37 | 49 |
<% end -%> |
38 |
- agentPaths["All Agents Index"] = <%= Utils.jsonify agents_path %>; |
|
39 |
- agentPaths["New Agent"] = <%= Utils.jsonify new_agent_path %>; |
|
40 |
- agentPaths["Account"] = <%= Utils.jsonify edit_user_registration_path %>; |
|
41 |
- agentPaths["Events Index"] = <%= Utils.jsonify events_path %>; |
|
42 |
- agentPaths["View Agent Diagram"] = <%= Utils.jsonify diagram_agents_path %>; |
|
43 |
- agentPaths["Run Event Propagation"] = { url: <%= Utils.jsonify propagate_agents_path %>, method: 'POST' }; |
|
44 |
- var agentNames = []; |
|
45 |
- $.each(agentPaths, function(name, v) { agentNames.push(name); }); |
|
46 | 50 |
</script> |
47 | 51 |
</body> |
48 | 52 |
</html> |
@@ -0,0 +1,31 @@ |
||
1 |
+<div class="row"> |
|
2 |
+ <div class="page-header"> |
|
3 |
+ <h2> |
|
4 |
+ Import a Public Scenario |
|
5 |
+ </h2> |
|
6 |
+ </div> |
|
7 |
+</div> |
|
8 |
+ |
|
9 |
+<div class='row'> |
|
10 |
+ <blockquote>You can import Scenarios, either from a <code>.json</code> file, or via a public |
|
11 |
+ Scenario URL. When you import a Scenario, Huginn will keep track of where it came from and |
|
12 |
+ later let you update it.</blockquote> |
|
13 |
+</div> |
|
14 |
+ |
|
15 |
+<div class='row'> |
|
16 |
+ <div class="col-md-4"> |
|
17 |
+ <div class="form-group"> |
|
18 |
+ <%= f.label :url, 'Option 1: Provide a Public Scenario URL' %> |
|
19 |
+ <%= f.text_field :url, :class => 'form-control', :placeholder => "Public Scenario URL" %> |
|
20 |
+ </div> |
|
21 |
+ |
|
22 |
+ <div class="form-group"> |
|
23 |
+ <%= f.label :file, 'Option 2: Upload a Scenario JSON File' %> |
|
24 |
+ <%= f.file_field :file, :class => 'form-control' %> |
|
25 |
+ </div> |
|
26 |
+ |
|
27 |
+ <div class='form-actions'> |
|
28 |
+ <%= f.submit "Start Import", :class => "btn btn-primary" %> |
|
29 |
+ </div> |
|
30 |
+ </div> |
|
31 |
+</div> |
@@ -0,0 +1,154 @@ |
||
1 |
+<div class="row"> |
|
2 |
+ <div class="col-md-12"> |
|
3 |
+ <% if @scenario_import.dangerous? %> |
|
4 |
+ <div class="alert alert-danger"> |
|
5 |
+ <span class='glyphicon glyphicon-warning-sign'></span> |
|
6 |
+ This Scenario contains one or more potentially dangerous Agents. |
|
7 |
+ These may be able to run local commands or execute code. |
|
8 |
+ Please be sure that you understand the Agent configurations before importing! |
|
9 |
+ </div> |
|
10 |
+ <% end %> |
|
11 |
+ |
|
12 |
+ <% if @scenario_import.existing_scenario.present? %> |
|
13 |
+ <div class="alert alert-warning"> |
|
14 |
+ <span class='glyphicon glyphicon-warning-sign'></span> |
|
15 |
+ This Scenario already exists in your system. The import will update your existing |
|
16 |
+ <span class='label label-info scenario'><%= @scenario_import.existing_scenario.name %></span> Scenario's title |
|
17 |
+ and |
|
18 |
+ description. Below you can customize how the individual agents get updated. |
|
19 |
+ </div> |
|
20 |
+ <% end %> |
|
21 |
+ |
|
22 |
+ <div class="page-header"> |
|
23 |
+ <h2> |
|
24 |
+ <%= @scenario_import.parsed_data["name"] %> |
|
25 |
+ <span class='text-muted'> |
|
26 |
+ (<%= pluralize @scenario_import.parsed_data["agents"].length, "Agent" %>; |
|
27 |
+ exported <%= time_ago_in_words Time.parse(@scenario_import.parsed_data["exported_at"]) %> ago) |
|
28 |
+ </span> |
|
29 |
+ </h2> |
|
30 |
+ </div> |
|
31 |
+ |
|
32 |
+ <% if @scenario_import.parsed_data["description"].present? %> |
|
33 |
+ <blockquote><%= @scenario_import.parsed_data["description"] %></blockquote> |
|
34 |
+ <% end %> |
|
35 |
+ |
|
36 |
+ </div> |
|
37 |
+</div> |
|
38 |
+ |
|
39 |
+<div class='agent-import-list'> |
|
40 |
+ <% @scenario_import.agent_diffs.each.with_index do |agent_diff, index| %> |
|
41 |
+ <div class='agent-import' data-index='<%= index %>'> |
|
42 |
+ |
|
43 |
+ <div class='row'> |
|
44 |
+ <div class='col-md-12'> |
|
45 |
+ <h3> |
|
46 |
+ <a href='#' data-toggle="modal" data-target="#agent_options_<%= index %>"><%= agent_diff.name.incoming %></a> |
|
47 |
+ <span class='text-muted'> |
|
48 |
+ (<%= agent_diff.type.incoming %><% " -- WARNING: this Agent's type has been changed. This import will likely fail!" if agent_diff.type.requires_merge? %>) |
|
49 |
+ </span> |
|
50 |
+ </h3> |
|
51 |
+ |
|
52 |
+ <% if agent_diff.agent_exists? %> |
|
53 |
+ <div class="instructions"> |
|
54 |
+ This Agent exists in your Huginn system. |
|
55 |
+ |
|
56 |
+ <% if agent_diff.requires_merge? %> |
|
57 |
+ Here are the differences between the incoming version and the one you have now. For each field, please |
|
58 |
+ select which value you'd like to keep. |
|
59 |
+ <% else %> |
|
60 |
+ It's already up-to-date. |
|
61 |
+ <% end %> |
|
62 |
+ </div> |
|
63 |
+ <% end %> |
|
64 |
+ </div> |
|
65 |
+ </div> |
|
66 |
+ |
|
67 |
+ <div class="modal fade" id="agent_options_<%= index %>" tabindex="-1" role="dialog" aria-labelledby="modalLabel<%= index %>" aria-hidden="true"> |
|
68 |
+ <div class="modal-dialog modal-lg"> |
|
69 |
+ <div class="modal-content"> |
|
70 |
+ <div class="modal-header"> |
|
71 |
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> |
|
72 |
+ <h4 class="modal-title" id="modalLabel<%= index %>">Options for '<%= agent_diff.name.updated %>'</h4> |
|
73 |
+ </div> |
|
74 |
+ <div class="modal-body"> |
|
75 |
+ <pre class='options'><%= Utils.pretty_jsonify agent_diff.options.incoming %></pre> |
|
76 |
+ </div> |
|
77 |
+ </div> |
|
78 |
+ </div> |
|
79 |
+ </div> |
|
80 |
+ |
|
81 |
+ <% agent_diff.each_field do |field, value, selection_options| %> |
|
82 |
+ <div class='row'> |
|
83 |
+ <div class='col-md-4'> |
|
84 |
+ <div class="form-group"> |
|
85 |
+ <%= label_tag "scenario_import[merges][#{index}][#{field}]", field.titleize %> |
|
86 |
+ <% if selection_options.present? %> |
|
87 |
+ <div> |
|
88 |
+ Your current Agent's value is: |
|
89 |
+ <span class='current'><%= selection_options.find { |s| s.last.to_s == value.current.to_s }.first %></span> |
|
90 |
+ </div> |
|
91 |
+ <%= select_tag "scenario_import[merges][#{index}][#{field}]", options_for_select(selection_options, value.updated), :class => 'form-control' %> |
|
92 |
+ <% else %> |
|
93 |
+ <div> |
|
94 |
+ Your current Agent's value is: <span class='current'><%= value.current.to_s %></span> |
|
95 |
+ </div> |
|
96 |
+ <%= text_field_tag "scenario_import[merges][#{index}][#{field}]", value.updated, :class => 'form-control' %> |
|
97 |
+ <% end %> |
|
98 |
+ </div> |
|
99 |
+ </div> |
|
100 |
+ </div> |
|
101 |
+ <% end %> |
|
102 |
+ |
|
103 |
+ <div class='row'> |
|
104 |
+ <% if agent_diff.options.requires_merge? %> |
|
105 |
+ <div class='col-md-12'> |
|
106 |
+ <label>Options</label> |
|
107 |
+ </div> |
|
108 |
+ |
|
109 |
+ <div class='col-md-6'> |
|
110 |
+ <textarea name="scenario_import[merges][<%= index %>][options]" rows='15' class="form-control live-json-editor"> |
|
111 |
+ <%= Utils.pretty_jsonify(agent_diff.options.updated) %> |
|
112 |
+ </textarea> |
|
113 |
+ </div> |
|
114 |
+ |
|
115 |
+ <div class='col-md-6'> |
|
116 |
+ <div> |
|
117 |
+ Your current options: |
|
118 |
+ </div> |
|
119 |
+ <pre class='options'><%= Utils.pretty_jsonify agent_diff.options.current %></pre> |
|
120 |
+ </div> |
|
121 |
+ <% end %> |
|
122 |
+ </div> |
|
123 |
+ </div> |
|
124 |
+ <% end %> |
|
125 |
+</div> |
|
126 |
+ |
|
127 |
+<div class='row'> |
|
128 |
+ <div class='col-md-12'> |
|
129 |
+ <div class="checkbox"> |
|
130 |
+ <%= f.label :do_import do %> |
|
131 |
+ <%= f.check_box :do_import %> I confirm that I want to import these Agents. |
|
132 |
+ <% end %> |
|
133 |
+ </div> |
|
134 |
+ |
|
135 |
+ <div class='form-actions'> |
|
136 |
+ <%= f.submit "Finish Import", :class => "btn btn-primary" %> |
|
137 |
+ </div> |
|
138 |
+ </div> |
|
139 |
+</div> |
|
140 |
+ |
|
141 |
+ |
|
142 |
+<script> |
|
143 |
+// $(function() { |
|
144 |
+// $('.agent-import-list .options-toggle').on('click', function (e) { |
|
145 |
+// e.preventDefault(); |
|
146 |
+// $(this).siblings('.options').slideToggle() |
|
147 |
+// if ($(this).text() == "Show Options") { |
|
148 |
+// $(this).text("Hide Options"); |
|
149 |
+// } else { |
|
150 |
+// $(this).text("Show Options"); |
|
151 |
+// } |
|
152 |
+// }); |
|
153 |
+// }); |
|
154 |
+</script> |
@@ -0,0 +1,32 @@ |
||
1 |
+<div class='container scenario-import'> |
|
2 |
+ <div class="row"> |
|
3 |
+ <div class="col-md-12"> |
|
4 |
+ <% if @scenario_import.errors.any? %> |
|
5 |
+ <div class="row well"> |
|
6 |
+ <h2><%= pluralize(@scenario_import.errors.count, "error") %> prohibited this Scenario from being imported:</h2> |
|
7 |
+ <% @scenario_import.errors.full_messages.each do |msg| %> |
|
8 |
+ <p class='text-warning'><%= msg %></p> |
|
9 |
+ <% end %> |
|
10 |
+ </div> |
|
11 |
+ <% end %> |
|
12 |
+ </div> |
|
13 |
+ </div> |
|
14 |
+ |
|
15 |
+ <%= form_for @scenario_import, :multipart => true do |f| %> |
|
16 |
+ <%= f.hidden_field :data %> |
|
17 |
+ |
|
18 |
+ <% if @scenario_import.step_one? %> |
|
19 |
+ <%= render 'step_one', :f => f %> |
|
20 |
+ <% elsif @scenario_import.step_two? %> |
|
21 |
+ <%= render 'step_two', :f => f %> |
|
22 |
+ <% end %> |
|
23 |
+ <% end %> |
|
24 |
+ |
|
25 |
+ <hr /> |
|
26 |
+ |
|
27 |
+ <div class="row"> |
|
28 |
+ <div class="col-md-12"> |
|
29 |
+ <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
30 |
+ </div> |
|
31 |
+ </div> |
|
32 |
+</div> |
@@ -0,0 +1,57 @@ |
||
1 |
+<%= form_for(@scenario, :method => @scenario.new_record? ? "POST" : "PUT") do |f| %> |
|
2 |
+ <% if @scenario.errors.any? %> |
|
3 |
+ <div class="row well"> |
|
4 |
+ <h2><%= pluralize(@scenario.errors.count, "error") %> prohibited this Scenario from being saved:</h2> |
|
5 |
+ <% @scenario.errors.full_messages.each do |msg| %> |
|
6 |
+ <p class='text-warning'><%= msg %></p> |
|
7 |
+ <% end %> |
|
8 |
+ </div> |
|
9 |
+ <% end %> |
|
10 |
+ |
|
11 |
+ <div class="row"> |
|
12 |
+ <div class="col-md-4"> |
|
13 |
+ <div class="form-group"> |
|
14 |
+ <%= f.label :name %> |
|
15 |
+ <%= f.text_field :name, :class => 'form-control', :placeholder => "Name your Scenario" %> |
|
16 |
+ </div> |
|
17 |
+ </div> |
|
18 |
+ </div> |
|
19 |
+ |
|
20 |
+ <div class="row"> |
|
21 |
+ <div class="col-md-8"> |
|
22 |
+ <div class="form-group"> |
|
23 |
+ <%= f.label :description, "Optional Description" %> |
|
24 |
+ <%= f.text_area :description, :rows => 10, :class => 'form-control', :placeholder => "Optionally describe what this Scenario will do. If this will be public, you should also include some contact information." %> |
|
25 |
+ </div> |
|
26 |
+ |
|
27 |
+ <div class="checkbox"> |
|
28 |
+ <%= f.label :public do %> |
|
29 |
+ <%= f.check_box :public %> Share this Scenario publicly |
|
30 |
+ <% end %> |
|
31 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="When selected, this Scenario and all Agents in it will be made public. An export URL will be available to share with other Huginn users. Be very careful that you do not have secret credentials stored in these Agents' options. Instead, use Credentials by reference."></span> |
|
32 |
+ </div> |
|
33 |
+ |
|
34 |
+ </div> |
|
35 |
+ </div> |
|
36 |
+ |
|
37 |
+ <div class="row"> |
|
38 |
+ <div class="col-md-4"> |
|
39 |
+ <div class="form-group"> |
|
40 |
+ <div> |
|
41 |
+ <%= f.label :agents %> |
|
42 |
+ <%= f.select(:agent_ids, |
|
43 |
+ options_for_select(current_user.agents.pluck(:name, :id), @scenario.agent_ids), |
|
44 |
+ {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %> |
|
45 |
+ </div> |
|
46 |
+ </div> |
|
47 |
+ </div> |
|
48 |
+ </div> |
|
49 |
+ |
|
50 |
+ <div class="row"> |
|
51 |
+ <div class="col-md-12"> |
|
52 |
+ <div class='form-actions' style='clear: both'> |
|
53 |
+ <%= f.submit "Save Scenario", :class => "btn btn-primary" %> |
|
54 |
+ </div> |
|
55 |
+ </div> |
|
56 |
+ </div> |
|
57 |
+<% end %> |
@@ -0,0 +1,21 @@ |
||
1 |
+<div class='container'> |
|
2 |
+ <div class='row'> |
|
3 |
+ <div class='col-md-12'> |
|
4 |
+ <div class="page-header"> |
|
5 |
+ <h2> |
|
6 |
+ Edit Scenario |
|
7 |
+ </h2> |
|
8 |
+ </div> |
|
9 |
+ |
|
10 |
+ <%= render 'form' %> |
|
11 |
+ |
|
12 |
+ <hr> |
|
13 |
+ |
|
14 |
+ <div class="row"> |
|
15 |
+ <div class="col-md-12"> |
|
16 |
+ <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
17 |
+ </div> |
|
18 |
+ </div> |
|
19 |
+ </div> |
|
20 |
+ </div> |
|
21 |
+</div> |
@@ -0,0 +1,50 @@ |
||
1 |
+<div class='container'> |
|
2 |
+ <div class='row'> |
|
3 |
+ <div class='col-md-12'> |
|
4 |
+ <div class="page-header"> |
|
5 |
+ <h2> |
|
6 |
+ Your Scenarios |
|
7 |
+ </h2> |
|
8 |
+ </div> |
|
9 |
+ |
|
10 |
+ <blockquote>Scenarios are named groups of Agents. Scenarios allow you to organize your agents, |
|
11 |
+ and to import and export sets of Agents to share.</blockquote> |
|
12 |
+ |
|
13 |
+ <table class='table table-striped'> |
|
14 |
+ <tr> |
|
15 |
+ <th>Name</th> |
|
16 |
+ <th>Agents</th> |
|
17 |
+ <th>Public</th> |
|
18 |
+ <th></th> |
|
19 |
+ </tr> |
|
20 |
+ |
|
21 |
+ <% @scenarios.each do |scenario| %> |
|
22 |
+ <tr> |
|
23 |
+ <td> |
|
24 |
+ <%= link_to(scenario.name, scenario) %> |
|
25 |
+ </td> |
|
26 |
+ <td><%= link_to pluralize(scenario.agents.count, "agent"), scenario %></td> |
|
27 |
+ <td><%= scenario.public? ? "yes" : "no" %></td> |
|
28 |
+ <td> |
|
29 |
+ <div class="btn-group btn-group-xs" style="float: right"> |
|
30 |
+ <%= link_to 'Show', scenario, class: "btn btn-default" %> |
|
31 |
+ <%= link_to 'Edit', edit_scenario_path(scenario), class: "btn btn-default" %> |
|
32 |
+ <%= link_to 'Share', share_scenario_path(scenario), class: "btn btn-default" %> |
|
33 |
+ <%= link_to 'Delete', scenario_path(scenario), method: :delete, data: { confirm: "This will remove the '#{scenario.name}' Scenerio from all Agents and delete it. Are you sure?" }, class: "btn btn-default" %> |
|
34 |
+ </div> |
|
35 |
+ </td> |
|
36 |
+ </tr> |
|
37 |
+ <% end %> |
|
38 |
+ </table> |
|
39 |
+ |
|
40 |
+ <%= paginate @scenarios, :theme => 'twitter-bootstrap' %> |
|
41 |
+ |
|
42 |
+ <br/> |
|
43 |
+ |
|
44 |
+ <div class="btn-group"> |
|
45 |
+ <%= link_to '<span class="glyphicon glyphicon-plus"></span> New Scenario'.html_safe, new_scenario_path, class: "btn btn-default" %> |
|
46 |
+ <%= link_to '<span class="glyphicon glyphicon-plus"></span> Import Scenario'.html_safe, new_scenario_imports_path, class: "btn btn-default" %> |
|
47 |
+ </div> |
|
48 |
+ </div> |
|
49 |
+ </div> |
|
50 |
+</div> |
@@ -0,0 +1,21 @@ |
||
1 |
+<div class='container'> |
|
2 |
+ <div class='row'> |
|
3 |
+ <div class='col-md-12'> |
|
4 |
+ <div class="page-header"> |
|
5 |
+ <h2> |
|
6 |
+ Create a new Scenario |
|
7 |
+ </h2> |
|
8 |
+ </div> |
|
9 |
+ |
|
10 |
+ <%= render 'form' %> |
|
11 |
+ |
|
12 |
+ <hr> |
|
13 |
+ |
|
14 |
+ <div class="row"> |
|
15 |
+ <div class="col-md-12"> |
|
16 |
+ <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
17 |
+ </div> |
|
18 |
+ </div> |
|
19 |
+ </div> |
|
20 |
+ </div> |
|
21 |
+</div> |
@@ -0,0 +1,33 @@ |
||
1 |
+<div class='container'> |
|
2 |
+ <div class='row'> |
|
3 |
+ <div class='col-md-12'> |
|
4 |
+ <div class="page-header"> |
|
5 |
+ <h2>Share <span class='label label-info scenario'><%= @scenario.name %></span> with the world</h2> |
|
6 |
+ </div> |
|
7 |
+ |
|
8 |
+ <p> |
|
9 |
+ <strong>Please be sure that none of the Agents in this Scenario have sensitive data in their settings before sharing!</strong> |
|
10 |
+ </p> |
|
11 |
+ |
|
12 |
+ <% if @scenario.public? %> |
|
13 |
+ <p> |
|
14 |
+ This Scenario is public. You can <%= link_to "download and share your export file", export_scenario_path(@scenario, :format => :json) %>, or give out this URL: |
|
15 |
+ </p> |
|
16 |
+ |
|
17 |
+ <form onsubmit='return false;'> |
|
18 |
+ <input type='text' class='form-control' value='<%= export_scenario_url(@scenario, :format => :json) %>' onclick="return this.select();"/> |
|
19 |
+ </form> |
|
20 |
+ <% else %> |
|
21 |
+ This Scenario is not public. You can share it by <%= link_to "downloading and sharing your export file", export_scenario_path(@scenario, :format => :json) %>. |
|
22 |
+ <% end %> |
|
23 |
+ |
|
24 |
+ <hr> |
|
25 |
+ |
|
26 |
+ <div class="row"> |
|
27 |
+ <div class="col-md-12"> |
|
28 |
+ <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenario_path(@scenario), class: "btn btn-default" %> |
|
29 |
+ </div> |
|
30 |
+ </div> |
|
31 |
+ </div> |
|
32 |
+ </div> |
|
33 |
+</div> |
@@ -0,0 +1,28 @@ |
||
1 |
+<div class='container'> |
|
2 |
+ <div class='row'> |
|
3 |
+ <div class='col-md-12'> |
|
4 |
+ <div class="page-header"> |
|
5 |
+ <h2><span class='label label-info scenario'><%= @scenario.name %></span> <%= "Public" if @scenario.public? %> Scenario</h2> |
|
6 |
+ </div> |
|
7 |
+ |
|
8 |
+ <% if @scenario.description.present? %> |
|
9 |
+ <blockquote><%= @scenario.description %></blockquote> |
|
10 |
+ <% end %> |
|
11 |
+ |
|
12 |
+ <%= render 'agents/table', :returnTo => scenario_path(@scenario) %> |
|
13 |
+ |
|
14 |
+ <br/> |
|
15 |
+ |
|
16 |
+ <div class="btn-group"> |
|
17 |
+ <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
18 |
+ <%= link_to '<span class="glyphicon glyphicon-random"></span> View Diagram'.html_safe, diagram_agents_path(:scenario_id => @scenario.to_param), class: "btn btn-default" %> |
|
19 |
+ <%= link_to '<span class="glyphicon glyphicon-edit"></span> Edit'.html_safe, edit_scenario_path(@scenario), class: "btn btn-default" %> |
|
20 |
+ <% if @scenario.source_url.present? %> |
|
21 |
+ <%= link_to '<span class="glyphicon glyphicon-plus"></span> Update'.html_safe, new_scenario_imports_path(:url => @scenario.source_url), class: "btn btn-default" %> |
|
22 |
+ <% end %> |
|
23 |
+ <%= link_to '<span class="glyphicon glyphicon-share-alt"></span> Share'.html_safe, share_scenario_path(@scenario), class: "btn btn-default" %> |
|
24 |
+ <%= link_to '<span class="glyphicon glyphicon-trash"></span> Delete'.html_safe, scenario_path(@scenario), method: :delete, data: { confirm: "This will remove the '#{@scenario.name}' Scenerio from all Agents and delete it. Are you sure?" }, class: "btn btn-default" %> |
|
25 |
+ </div> |
|
26 |
+ </div> |
|
27 |
+ </div> |
|
28 |
+</div> |
@@ -3,6 +3,7 @@ Huginn::Application.routes.draw do |
||
3 | 3 |
member do |
4 | 4 |
post :run |
5 | 5 |
post :handle_details_post |
6 |
+ put :leave_scenario |
|
6 | 7 |
delete :remove_events |
7 | 8 |
end |
8 | 9 |
|
@@ -26,6 +27,17 @@ Huginn::Application.routes.draw do |
||
26 | 27 |
end |
27 | 28 |
end |
28 | 29 |
|
30 |
+ resources :scenarios do |
|
31 |
+ collection do |
|
32 |
+ resource :scenario_imports, :only => [:new, :create] |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ member do |
|
36 |
+ get :share |
|
37 |
+ get :export |
|
38 |
+ end |
|
39 |
+ end |
|
40 |
+ |
|
29 | 41 |
resources :user_credentials, :except => :show |
30 | 42 |
|
31 | 43 |
get "/worker_status" => "worker_status#show" |
@@ -0,0 +1,12 @@ |
||
1 |
+class CreateScenarios < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ create_table :scenarios do |t| |
|
4 |
+ t.string :name, :null => false |
|
5 |
+ t.integer :user_id, :null => false |
|
6 |
+ |
|
7 |
+ t.timestamps |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ add_column :users, :scenario_count, :integer, :null => false, :default => 0 |
|
11 |
+ end |
|
12 |
+end |
@@ -0,0 +1,10 @@ |
||
1 |
+class CreateScenarioMemberships < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ create_table :scenario_memberships do |t| |
|
4 |
+ t.integer :agent_id, :null => false |
|
5 |
+ t.integer :scenario_id, :null => false |
|
6 |
+ |
|
7 |
+ t.timestamps |
|
8 |
+ end |
|
9 |
+ end |
|
10 |
+end |
@@ -0,0 +1,8 @@ |
||
1 |
+class AddFieldsToScenarios < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ add_column :scenarios, :description, :text |
|
4 |
+ add_column :scenarios, :public, :boolean, :default => false, :null => false |
|
5 |
+ add_column :scenarios, :guid, :string, :null => false |
|
6 |
+ add_column :scenarios, :source_url, :string |
|
7 |
+ end |
|
8 |
+end |
@@ -0,0 +1,7 @@ |
||
1 |
+class AddIndicesToScenarios < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ add_index :scenarios, [:user_id, :guid], :unique => true |
|
4 |
+ add_index :scenario_memberships, :agent_id |
|
5 |
+ add_index :scenario_memberships, :scenario_id |
|
6 |
+ end |
|
7 |
+end |
@@ -0,0 +1,15 @@ |
||
1 |
+class AddGuidToAgents < ActiveRecord::Migration |
|
2 |
+ class Agent < ActiveRecord::Base; end |
|
3 |
+ |
|
4 |
+ def change |
|
5 |
+ add_column :agents, :guid, :string |
|
6 |
+ |
|
7 |
+ Agent.find_each do |agent| |
|
8 |
+ agent.update_attribute :guid, SecureRandom.hex |
|
9 |
+ end |
|
10 |
+ |
|
11 |
+ change_column_null :agents, :guid, false |
|
12 |
+ |
|
13 |
+ add_index :agents, :guid |
|
14 |
+ end |
|
15 |
+end |
@@ -9,23 +9,23 @@ |
||
9 | 9 |
# from scratch. The latter is a flawed and unsustainable approach (the more migrations |
10 | 10 |
# you'll amass, the slower it'll run and the greater likelihood for issues). |
11 | 11 |
# |
12 |
-# It's strongly recommended to check this file into your version control system. |
|
12 |
+# It's strongly recommended that you check this file into your version control system. |
|
13 | 13 |
|
14 |
-ActiveRecord::Schema.define(:version => 20140408150825) do |
|
14 |
+ActiveRecord::Schema.define(version: 20140605032822) do |
|
15 | 15 |
|
16 |
- create_table "agent_logs", :force => true do |t| |
|
17 |
- t.integer "agent_id", :null => false |
|
18 |
- t.text "message", :null => false |
|
19 |
- t.integer "level", :default => 3, :null => false |
|
16 |
+ create_table "agent_logs", force: true do |t| |
|
17 |
+ t.integer "agent_id", null: false |
|
18 |
+ t.text "message", limit: 16777215, null: false |
|
19 |
+ t.integer "level", default: 3, null: false |
|
20 | 20 |
t.integer "inbound_event_id" |
21 | 21 |
t.integer "outbound_event_id" |
22 |
- t.datetime "created_at", :null => false |
|
23 |
- t.datetime "updated_at", :null => false |
|
22 |
+ t.datetime "created_at", null: false |
|
23 |
+ t.datetime "updated_at", null: false |
|
24 | 24 |
end |
25 | 25 |
|
26 |
- create_table "agents", :force => true do |t| |
|
26 |
+ create_table "agents", force: true do |t| |
|
27 | 27 |
t.integer "user_id" |
28 |
- t.text "options" |
|
28 |
+ t.text "options", limit: 16777215 |
|
29 | 29 |
t.string "type" |
30 | 30 |
t.string "name" |
31 | 31 |
t.string "schedule" |
@@ -33,98 +33,124 @@ ActiveRecord::Schema.define(:version => 20140408150825) do |
||
33 | 33 |
t.datetime "last_check_at" |
34 | 34 |
t.datetime "last_receive_at" |
35 | 35 |
t.integer "last_checked_event_id" |
36 |
- t.datetime "created_at", :null => false |
|
37 |
- t.datetime "updated_at", :null => false |
|
38 |
- t.text "memory", :limit => 2147483647 |
|
36 |
+ t.datetime "created_at", null: false |
|
37 |
+ t.datetime "updated_at", null: false |
|
38 |
+ t.text "memory", limit: 2147483647 |
|
39 | 39 |
t.datetime "last_web_request_at" |
40 |
- t.integer "keep_events_for", :default => 0, :null => false |
|
41 | 40 |
t.datetime "last_event_at" |
42 | 41 |
t.datetime "last_error_log_at" |
43 |
- t.boolean "propagate_immediately", :default => false, :null => false |
|
44 |
- t.boolean "disabled", :default => false, :null => false |
|
42 |
+ t.integer "keep_events_for", default: 0, null: false |
|
43 |
+ t.boolean "propagate_immediately", default: false, null: false |
|
44 |
+ t.boolean "disabled", default: false, null: false |
|
45 |
+ t.string "guid", null: false |
|
45 | 46 |
end |
46 | 47 |
|
47 |
- add_index "agents", ["schedule"], :name => "index_agents_on_schedule" |
|
48 |
- add_index "agents", ["type"], :name => "index_agents_on_type" |
|
49 |
- add_index "agents", ["user_id", "created_at"], :name => "index_agents_on_user_id_and_created_at" |
|
48 |
+ add_index "agents", ["guid"], name: "index_agents_on_guid", using: :btree |
|
49 |
+ add_index "agents", ["schedule"], name: "index_agents_on_schedule", using: :btree |
|
50 |
+ add_index "agents", ["type"], name: "index_agents_on_type", using: :btree |
|
51 |
+ add_index "agents", ["user_id", "created_at"], name: "index_agents_on_user_id_and_created_at", using: :btree |
|
50 | 52 |
|
51 |
- create_table "delayed_jobs", :force => true do |t| |
|
52 |
- t.integer "priority", :default => 0 |
|
53 |
- t.integer "attempts", :default => 0 |
|
54 |
- t.text "handler", :limit => 16777215 |
|
55 |
- t.text "last_error" |
|
53 |
+ create_table "delayed_jobs", force: true do |t| |
|
54 |
+ t.integer "priority", default: 0 |
|
55 |
+ t.integer "attempts", default: 0 |
|
56 |
+ t.text "handler", limit: 16777215 |
|
57 |
+ t.text "last_error", limit: 16777215 |
|
56 | 58 |
t.datetime "run_at" |
57 | 59 |
t.datetime "locked_at" |
58 | 60 |
t.datetime "failed_at" |
59 | 61 |
t.string "locked_by" |
60 | 62 |
t.string "queue" |
61 |
- t.datetime "created_at", :null => false |
|
62 |
- t.datetime "updated_at", :null => false |
|
63 |
+ t.datetime "created_at", null: false |
|
64 |
+ t.datetime "updated_at", null: false |
|
63 | 65 |
end |
64 | 66 |
|
65 |
- add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority" |
|
67 |
+ add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree |
|
66 | 68 |
|
67 |
- create_table "events", :force => true do |t| |
|
69 |
+ create_table "events", force: true do |t| |
|
68 | 70 |
t.integer "user_id" |
69 | 71 |
t.integer "agent_id" |
70 |
- t.decimal "lat", :precision => 15, :scale => 10 |
|
71 |
- t.decimal "lng", :precision => 15, :scale => 10 |
|
72 |
- t.text "payload", :limit => 16777215 |
|
73 |
- t.datetime "created_at", :null => false |
|
74 |
- t.datetime "updated_at", :null => false |
|
72 |
+ t.decimal "lat", precision: 15, scale: 10 |
|
73 |
+ t.decimal "lng", precision: 15, scale: 10 |
|
74 |
+ t.text "payload", limit: 2147483647 |
|
75 |
+ t.datetime "created_at", null: false |
|
76 |
+ t.datetime "updated_at", null: false |
|
75 | 77 |
t.datetime "expires_at" |
76 | 78 |
end |
77 | 79 |
|
78 |
- add_index "events", ["agent_id", "created_at"], :name => "index_events_on_agent_id_and_created_at" |
|
79 |
- add_index "events", ["expires_at"], :name => "index_events_on_expires_at" |
|
80 |
- add_index "events", ["user_id", "created_at"], :name => "index_events_on_user_id_and_created_at" |
|
80 |
+ add_index "events", ["agent_id", "created_at"], name: "index_events_on_agent_id_and_created_at", using: :btree |
|
81 |
+ add_index "events", ["expires_at"], name: "index_events_on_expires_at", using: :btree |
|
82 |
+ add_index "events", ["user_id", "created_at"], name: "index_events_on_user_id_and_created_at", using: :btree |
|
81 | 83 |
|
82 |
- create_table "links", :force => true do |t| |
|
84 |
+ create_table "links", force: true do |t| |
|
83 | 85 |
t.integer "source_id" |
84 | 86 |
t.integer "receiver_id" |
85 |
- t.datetime "created_at", :null => false |
|
86 |
- t.datetime "updated_at", :null => false |
|
87 |
- t.integer "event_id_at_creation", :default => 0, :null => false |
|
87 |
+ t.datetime "created_at", null: false |
|
88 |
+ t.datetime "updated_at", null: false |
|
89 |
+ t.integer "event_id_at_creation", default: 0, null: false |
|
88 | 90 |
end |
89 | 91 |
|
90 |
- add_index "links", ["receiver_id", "source_id"], :name => "index_links_on_receiver_id_and_source_id" |
|
91 |
- add_index "links", ["source_id", "receiver_id"], :name => "index_links_on_source_id_and_receiver_id" |
|
92 |
+ add_index "links", ["receiver_id", "source_id"], name: "index_links_on_receiver_id_and_source_id", using: :btree |
|
93 |
+ add_index "links", ["source_id", "receiver_id"], name: "index_links_on_source_id_and_receiver_id", using: :btree |
|
92 | 94 |
|
93 |
- create_table "user_credentials", :force => true do |t| |
|
94 |
- t.integer "user_id", :null => false |
|
95 |
- t.string "credential_name", :null => false |
|
96 |
- t.text "credential_value", :null => false |
|
97 |
- t.datetime "created_at", :null => false |
|
98 |
- t.datetime "updated_at", :null => false |
|
99 |
- t.string "mode", :default => "text", :null => false |
|
95 |
+ create_table "scenario_memberships", force: true do |t| |
|
96 |
+ t.integer "agent_id", null: false |
|
97 |
+ t.integer "scenario_id", null: false |
|
98 |
+ t.datetime "created_at" |
|
99 |
+ t.datetime "updated_at" |
|
100 | 100 |
end |
101 | 101 |
|
102 |
- add_index "user_credentials", ["user_id", "credential_name"], :name => "index_user_credentials_on_user_id_and_credential_name", :unique => true |
|
102 |
+ add_index "scenario_memberships", ["agent_id"], name: "index_scenario_memberships_on_agent_id", using: :btree |
|
103 |
+ add_index "scenario_memberships", ["scenario_id"], name: "index_scenario_memberships_on_scenario_id", using: :btree |
|
103 | 104 |
|
104 |
- create_table "users", :force => true do |t| |
|
105 |
- t.string "email", :default => "", :null => false |
|
106 |
- t.string "encrypted_password", :default => "", :null => false |
|
105 |
+ create_table "scenarios", force: true do |t| |
|
106 |
+ t.string "name", null: false |
|
107 |
+ t.integer "user_id", null: false |
|
108 |
+ t.datetime "created_at" |
|
109 |
+ t.datetime "updated_at" |
|
110 |
+ t.text "description" |
|
111 |
+ t.boolean "public", default: false, null: false |
|
112 |
+ t.string "guid", null: false |
|
113 |
+ t.string "source_url" |
|
114 |
+ end |
|
115 |
+ |
|
116 |
+ add_index "scenarios", ["user_id", "guid"], name: "index_scenarios_on_user_id_and_guid", unique: true, using: :btree |
|
117 |
+ |
|
118 |
+ create_table "user_credentials", force: true do |t| |
|
119 |
+ t.integer "user_id", null: false |
|
120 |
+ t.string "credential_name", null: false |
|
121 |
+ t.text "credential_value", null: false |
|
122 |
+ t.datetime "created_at", null: false |
|
123 |
+ t.datetime "updated_at", null: false |
|
124 |
+ t.string "mode", default: "text", null: false |
|
125 |
+ end |
|
126 |
+ |
|
127 |
+ add_index "user_credentials", ["user_id", "credential_name"], name: "index_user_credentials_on_user_id_and_credential_name", unique: true, using: :btree |
|
128 |
+ |
|
129 |
+ create_table "users", force: true do |t| |
|
130 |
+ t.string "email", default: "", null: false |
|
131 |
+ t.string "encrypted_password", default: "", null: false |
|
107 | 132 |
t.string "reset_password_token" |
108 | 133 |
t.datetime "reset_password_sent_at" |
109 | 134 |
t.datetime "remember_created_at" |
110 |
- t.integer "sign_in_count", :default => 0 |
|
135 |
+ t.integer "sign_in_count", default: 0 |
|
111 | 136 |
t.datetime "current_sign_in_at" |
112 | 137 |
t.datetime "last_sign_in_at" |
113 | 138 |
t.string "current_sign_in_ip" |
114 | 139 |
t.string "last_sign_in_ip" |
115 |
- t.datetime "created_at", :null => false |
|
116 |
- t.datetime "updated_at", :null => false |
|
117 |
- t.boolean "admin", :default => false, :null => false |
|
118 |
- t.integer "failed_attempts", :default => 0 |
|
140 |
+ t.datetime "created_at", null: false |
|
141 |
+ t.datetime "updated_at", null: false |
|
142 |
+ t.boolean "admin", default: false, null: false |
|
143 |
+ t.integer "failed_attempts", default: 0 |
|
119 | 144 |
t.string "unlock_token" |
120 | 145 |
t.datetime "locked_at" |
121 |
- t.string "username", :null => false |
|
122 |
- t.string "invitation_code", :null => false |
|
146 |
+ t.string "username", null: false |
|
147 |
+ t.string "invitation_code", null: false |
|
148 |
+ t.integer "scenario_count", default: 0, null: false |
|
123 | 149 |
end |
124 | 150 |
|
125 |
- add_index "users", ["email"], :name => "index_users_on_email", :unique => true |
|
126 |
- add_index "users", ["reset_password_token"], :name => "index_users_on_reset_password_token", :unique => true |
|
127 |
- add_index "users", ["unlock_token"], :name => "index_users_on_unlock_token", :unique => true |
|
128 |
- add_index "users", ["username"], :name => "index_users_on_username", :unique => true |
|
151 |
+ add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree |
|
152 |
+ add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree |
|
153 |
+ add_index "users", ["unlock_token"], name: "index_users_on_unlock_token", unique: true, using: :btree |
|
154 |
+ add_index "users", ["username"], name: "index_users_on_username", unique: true, using: :btree |
|
129 | 155 |
|
130 | 156 |
end |
@@ -0,0 +1,54 @@ |
||
1 |
+class AgentsExporter |
|
2 |
+ attr_accessor :options |
|
3 |
+ |
|
4 |
+ def initialize(options) |
|
5 |
+ self.options = options |
|
6 |
+ end |
|
7 |
+ |
|
8 |
+ # Filename should have no commas or special characters to support Content-Disposition on older browsers. |
|
9 |
+ def filename |
|
10 |
+ ((options[:name] || '').downcase.gsub(/[^a-z0-9_-]/, '-').gsub(/-+/, '-').gsub(/^-|-$/, '').presence || 'exported-agents') + ".json" |
|
11 |
+ end |
|
12 |
+ |
|
13 |
+ def as_json(opts = {}) |
|
14 |
+ { |
|
15 |
+ :name => options[:name].presence || 'No name provided', |
|
16 |
+ :description => options[:description].presence || 'No description provided', |
|
17 |
+ :source_url => options[:source_url], |
|
18 |
+ :guid => options[:guid], |
|
19 |
+ :exported_at => Time.now.utc.iso8601, |
|
20 |
+ :agents => agents.map { |agent| agent_as_json(agent) }, |
|
21 |
+ :links => links |
|
22 |
+ } |
|
23 |
+ end |
|
24 |
+ |
|
25 |
+ def agents |
|
26 |
+ options[:agents].to_a |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ def links |
|
30 |
+ agent_ids = agents.map(&:id) |
|
31 |
+ |
|
32 |
+ contained_links = agents.map.with_index do |agent, index| |
|
33 |
+ agent.links_as_source.where(:receiver_id => agent_ids).map do |link| |
|
34 |
+ { :source => index, :receiver => agent_ids.index(link.receiver_id) } |
|
35 |
+ end |
|
36 |
+ end |
|
37 |
+ |
|
38 |
+ contained_links.flatten.compact |
|
39 |
+ end |
|
40 |
+ |
|
41 |
+ def agent_as_json(agent) |
|
42 |
+ { |
|
43 |
+ :type => agent.type, |
|
44 |
+ :name => agent.name, |
|
45 |
+ :disabled => agent.disabled, |
|
46 |
+ :guid => agent.guid, |
|
47 |
+ :options => agent.options |
|
48 |
+ }.tap do |options| |
|
49 |
+ options[:schedule] = agent.schedule if agent.can_be_scheduled? |
|
50 |
+ options[:keep_events_for] = agent.keep_events_for if agent.can_create_events? |
|
51 |
+ options[:propagate_immediately] = agent.propagate_immediately if agent.can_receive_events? |
|
52 |
+ end |
|
53 |
+ end |
|
54 |
+end |
@@ -34,6 +34,47 @@ describe AgentsController do |
||
34 | 34 |
end |
35 | 35 |
end |
36 | 36 |
|
37 |
+ describe "POST run" do |
|
38 |
+ it "triggers Agent.async_check with the Agent's ID" do |
|
39 |
+ sign_in users(:bob) |
|
40 |
+ mock(Agent).async_check(agents(:bob_manual_event_agent).id) |
|
41 |
+ post :run, :id => agents(:bob_manual_event_agent).to_param |
|
42 |
+ end |
|
43 |
+ |
|
44 |
+ it "can only be accessed by the Agent's owner" do |
|
45 |
+ sign_in users(:jane) |
|
46 |
+ lambda { |
|
47 |
+ post :run, :id => agents(:bob_manual_event_agent).to_param |
|
48 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
49 |
+ end |
|
50 |
+ end |
|
51 |
+ |
|
52 |
+ describe "POST remove_events" do |
|
53 |
+ it "deletes all events created by the given Agent" do |
|
54 |
+ sign_in users(:bob) |
|
55 |
+ agent_event = events(:bob_website_agent_event).id |
|
56 |
+ other_event = events(:jane_website_agent_event).id |
|
57 |
+ post :remove_events, :id => agents(:bob_website_agent).to_param |
|
58 |
+ Event.where(:id => agent_event).count.should == 0 |
|
59 |
+ Event.where(:id => other_event).count.should == 1 |
|
60 |
+ end |
|
61 |
+ |
|
62 |
+ it "can only be accessed by the Agent's owner" do |
|
63 |
+ sign_in users(:jane) |
|
64 |
+ lambda { |
|
65 |
+ post :remove_events, :id => agents(:bob_website_agent).to_param |
|
66 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
67 |
+ end |
|
68 |
+ end |
|
69 |
+ |
|
70 |
+ describe "POST propagate" do |
|
71 |
+ it "runs event propagation for all Agents" do |
|
72 |
+ sign_in users(:bob) |
|
73 |
+ mock.proxy(Agent).receive! |
|
74 |
+ post :propagate |
|
75 |
+ end |
|
76 |
+ end |
|
77 |
+ |
|
37 | 78 |
describe "GET show" do |
38 | 79 |
it "only shows Agents for the current user" do |
39 | 80 |
sign_in users(:bob) |
@@ -152,18 +193,80 @@ describe AgentsController do |
||
152 | 193 |
}.should raise_error(ActiveRecord::RecordNotFound) |
153 | 194 |
end |
154 | 195 |
|
196 |
+ it "accepts JSON requests" do |
|
197 |
+ sign_in users(:bob) |
|
198 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :format => :json |
|
199 |
+ agents(:bob_website_agent).reload.name.should == "New name" |
|
200 |
+ JSON.parse(response.body)['name'].should == "New name" |
|
201 |
+ response.should be_success |
|
202 |
+ end |
|
203 |
+ |
|
155 | 204 |
it "will not accept Agent sources owned by other users" do |
156 | 205 |
sign_in users(:bob) |
157 | 206 |
post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:source_ids => [agents(:jane_weather_agent).id]) |
158 | 207 |
assigns(:agent).should have(1).errors_on(:sources) |
159 | 208 |
end |
160 | 209 |
|
210 |
+ it "will not accept Scenarios owned by other users" do |
|
211 |
+ sign_in users(:bob) |
|
212 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:scenario_ids => [scenarios(:jane_weather).id]) |
|
213 |
+ assigns(:agent).should have(1).errors_on(:scenarios) |
|
214 |
+ end |
|
215 |
+ |
|
161 | 216 |
it "shows errors" do |
162 | 217 |
sign_in users(:bob) |
163 | 218 |
post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "") |
164 | 219 |
assigns(:agent).should have(1).errors_on(:name) |
165 | 220 |
response.should render_template("edit") |
166 | 221 |
end |
222 |
+ |
|
223 |
+ describe "redirecting back" do |
|
224 |
+ before do |
|
225 |
+ sign_in users(:bob) |
|
226 |
+ end |
|
227 |
+ |
|
228 |
+ it "can redirect back to the show path" do |
|
229 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "show" |
|
230 |
+ response.should redirect_to(agent_path(agents(:bob_website_agent))) |
|
231 |
+ end |
|
232 |
+ |
|
233 |
+ it "redirect back to the index path by default" do |
|
234 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name") |
|
235 |
+ response.should redirect_to(agents_path) |
|
236 |
+ end |
|
237 |
+ |
|
238 |
+ it "accepts return paths to scenarios" do |
|
239 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "/scenarios/2" |
|
240 |
+ response.should redirect_to("/scenarios/2") |
|
241 |
+ end |
|
242 |
+ |
|
243 |
+ it "sanitizes return paths" do |
|
244 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "/scenar" |
|
245 |
+ response.should redirect_to(agents_path) |
|
246 |
+ |
|
247 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "http://google.com" |
|
248 |
+ response.should redirect_to(agents_path) |
|
249 |
+ |
|
250 |
+ post :update, :id => agents(:bob_website_agent).to_param, :agent => valid_attributes(:name => "New name"), :return => "javascript:alert(1)" |
|
251 |
+ response.should redirect_to(agents_path) |
|
252 |
+ end |
|
253 |
+ end |
|
254 |
+ end |
|
255 |
+ |
|
256 |
+ describe "PUT leave_scenario" do |
|
257 |
+ it "removes an Agent from the given Scenario for the current user" do |
|
258 |
+ sign_in users(:bob) |
|
259 |
+ |
|
260 |
+ agents(:bob_weather_agent).scenarios.should include(scenarios(:bob_weather)) |
|
261 |
+ put :leave_scenario, :id => agents(:bob_weather_agent).to_param, :scenario_id => scenarios(:bob_weather).to_param |
|
262 |
+ agents(:bob_weather_agent).scenarios.should_not include(scenarios(:bob_weather)) |
|
263 |
+ |
|
264 |
+ Scenario.where(:id => scenarios(:bob_weather).id).should exist |
|
265 |
+ |
|
266 |
+ lambda { |
|
267 |
+ put :leave_scenario, :id => agents(:jane_weather_agent).to_param, :scenario_id => scenarios(:jane_weather).to_param |
|
268 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
269 |
+ end |
|
167 | 270 |
end |
168 | 271 |
|
169 | 272 |
describe "DELETE destroy" do |
@@ -0,0 +1,26 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe ScenarioImportsController do |
|
4 |
+ before do |
|
5 |
+ sign_in users(:bob) |
|
6 |
+ end |
|
7 |
+ |
|
8 |
+ describe "GET new" do |
|
9 |
+ it "initializes a new ScenarioImport and renders new" do |
|
10 |
+ get :new |
|
11 |
+ assigns(:scenario_import).should be_a(ScenarioImport) |
|
12 |
+ response.should render_template(:new) |
|
13 |
+ end |
|
14 |
+ end |
|
15 |
+ |
|
16 |
+ describe "POST create" do |
|
17 |
+ it "initializes a ScenarioImport for current_user, passing in params" do |
|
18 |
+ post :create, :scenario_import => { :url => "bad url" } |
|
19 |
+ assigns(:scenario_import).user.should == users(:bob) |
|
20 |
+ assigns(:scenario_import).url.should == "bad url" |
|
21 |
+ assigns(:scenario_import).should_not be_valid |
|
22 |
+ response.should render_template(:new) |
|
23 |
+ end |
|
24 |
+ end |
|
25 |
+end |
|
26 |
+ |
@@ -0,0 +1,152 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe ScenariosController do |
|
4 |
+ def valid_attributes(options = {}) |
|
5 |
+ { :name => "some_name" }.merge(options) |
|
6 |
+ end |
|
7 |
+ |
|
8 |
+ before do |
|
9 |
+ sign_in users(:bob) |
|
10 |
+ end |
|
11 |
+ |
|
12 |
+ describe "GET index" do |
|
13 |
+ it "only returns Scenarios for the current user" do |
|
14 |
+ get :index |
|
15 |
+ assigns(:scenarios).all? {|i| i.user.should == users(:bob) }.should be_true |
|
16 |
+ end |
|
17 |
+ end |
|
18 |
+ |
|
19 |
+ describe "GET show" do |
|
20 |
+ it "only shows Scenarios for the current user" do |
|
21 |
+ get :show, :id => scenarios(:bob_weather).to_param |
|
22 |
+ assigns(:scenario).should eq(scenarios(:bob_weather)) |
|
23 |
+ |
|
24 |
+ lambda { |
|
25 |
+ get :show, :id => scenarios(:jane_weather).to_param |
|
26 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ it "loads Agents for the requested Scenario" do |
|
30 |
+ get :show, :id => scenarios(:bob_weather).to_param |
|
31 |
+ assigns(:agents).pluck(:id).should eq(scenarios(:bob_weather).agents.pluck(:id)) |
|
32 |
+ end |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ describe "GET share" do |
|
36 |
+ it "only displays Scenario share information for the current user" do |
|
37 |
+ get :share, :id => scenarios(:bob_weather).to_param |
|
38 |
+ assigns(:scenario).should eq(scenarios(:bob_weather)) |
|
39 |
+ |
|
40 |
+ lambda { |
|
41 |
+ get :share, :id => scenarios(:jane_weather).to_param |
|
42 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
43 |
+ end |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ describe "GET export" do |
|
47 |
+ it "returns a JSON file download from an instantiated AgentsExporter" do |
|
48 |
+ get :export, :id => scenarios(:bob_weather).to_param |
|
49 |
+ assigns(:exporter).options[:name].should == scenarios(:bob_weather).name |
|
50 |
+ assigns(:exporter).options[:description].should == scenarios(:bob_weather).description |
|
51 |
+ assigns(:exporter).options[:agents].should == scenarios(:bob_weather).agents |
|
52 |
+ assigns(:exporter).options[:guid].should == scenarios(:bob_weather).guid |
|
53 |
+ assigns(:exporter).options[:source_url].should be_false |
|
54 |
+ response.headers['Content-Disposition'].should == 'attachment; filename="bob-s-weather-alert-scenario.json"' |
|
55 |
+ response.headers['Content-Type'].should == 'application/json; charset=utf-8' |
|
56 |
+ JSON.parse(response.body)["name"].should == scenarios(:bob_weather).name |
|
57 |
+ end |
|
58 |
+ |
|
59 |
+ it "only exports private Scenarios for the current user" do |
|
60 |
+ get :export, :id => scenarios(:bob_weather).to_param |
|
61 |
+ assigns(:scenario).should eq(scenarios(:bob_weather)) |
|
62 |
+ |
|
63 |
+ lambda { |
|
64 |
+ get :export, :id => scenarios(:jane_weather).to_param |
|
65 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
66 |
+ end |
|
67 |
+ |
|
68 |
+ describe "public exports" do |
|
69 |
+ before do |
|
70 |
+ scenarios(:jane_weather).update_attribute :public, true |
|
71 |
+ end |
|
72 |
+ |
|
73 |
+ it "exports public scenarios for other users when logged in" do |
|
74 |
+ get :export, :id => scenarios(:jane_weather).to_param |
|
75 |
+ assigns(:scenario).should eq(scenarios(:jane_weather)) |
|
76 |
+ assigns(:exporter).options[:source_url].should == export_scenario_url(scenarios(:jane_weather)) |
|
77 |
+ end |
|
78 |
+ |
|
79 |
+ it "exports public scenarios for other users when logged out" do |
|
80 |
+ sign_out :user |
|
81 |
+ get :export, :id => scenarios(:jane_weather).to_param |
|
82 |
+ assigns(:scenario).should eq(scenarios(:jane_weather)) |
|
83 |
+ assigns(:exporter).options[:source_url].should == export_scenario_url(scenarios(:jane_weather)) |
|
84 |
+ end |
|
85 |
+ end |
|
86 |
+ end |
|
87 |
+ |
|
88 |
+ describe "GET edit" do |
|
89 |
+ it "only shows Scenarios for the current user" do |
|
90 |
+ get :edit, :id => scenarios(:bob_weather).to_param |
|
91 |
+ assigns(:scenario).should eq(scenarios(:bob_weather)) |
|
92 |
+ |
|
93 |
+ lambda { |
|
94 |
+ get :edit, :id => scenarios(:jane_weather).to_param |
|
95 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
96 |
+ end |
|
97 |
+ end |
|
98 |
+ |
|
99 |
+ describe "POST create" do |
|
100 |
+ it "creates Scenarios for the current user" do |
|
101 |
+ expect { |
|
102 |
+ post :create, :scenario => valid_attributes |
|
103 |
+ }.to change { users(:bob).scenarios.count }.by(1) |
|
104 |
+ end |
|
105 |
+ |
|
106 |
+ it "shows errors" do |
|
107 |
+ expect { |
|
108 |
+ post :create, :scenario => valid_attributes(:name => "") |
|
109 |
+ }.not_to change { users(:bob).scenarios.count } |
|
110 |
+ assigns(:scenario).should have(1).errors_on(:name) |
|
111 |
+ response.should render_template("new") |
|
112 |
+ end |
|
113 |
+ |
|
114 |
+ it "will not create Scenarios for other users" do |
|
115 |
+ expect { |
|
116 |
+ post :create, :scenario => valid_attributes(:user_id => users(:jane).id) |
|
117 |
+ }.to raise_error(ActiveModel::MassAssignmentSecurity::Error) |
|
118 |
+ end |
|
119 |
+ end |
|
120 |
+ |
|
121 |
+ describe "PUT update" do |
|
122 |
+ it "updates attributes on Scenarios for the current user" do |
|
123 |
+ post :update, :id => scenarios(:bob_weather).to_param, :scenario => { :name => "new_name", :public => "1" } |
|
124 |
+ response.should redirect_to(scenario_path(scenarios(:bob_weather))) |
|
125 |
+ scenarios(:bob_weather).reload.name.should == "new_name" |
|
126 |
+ scenarios(:bob_weather).should be_public |
|
127 |
+ |
|
128 |
+ lambda { |
|
129 |
+ post :update, :id => scenarios(:jane_weather).to_param, :scenario => { :name => "new_name" } |
|
130 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
131 |
+ scenarios(:jane_weather).reload.name.should_not == "new_name" |
|
132 |
+ end |
|
133 |
+ |
|
134 |
+ it "shows errors" do |
|
135 |
+ post :update, :id => scenarios(:bob_weather).to_param, :scenario => { :name => "" } |
|
136 |
+ assigns(:scenario).should have(1).errors_on(:name) |
|
137 |
+ response.should render_template("edit") |
|
138 |
+ end |
|
139 |
+ end |
|
140 |
+ |
|
141 |
+ describe "DELETE destroy" do |
|
142 |
+ it "destroys only Scenarios owned by the current user" do |
|
143 |
+ expect { |
|
144 |
+ delete :destroy, :id => scenarios(:bob_weather).to_param |
|
145 |
+ }.to change(Scenario, :count).by(-1) |
|
146 |
+ |
|
147 |
+ lambda { |
|
148 |
+ delete :destroy, :id => scenarios(:jane_weather).to_param |
|
149 |
+ }.should raise_error(ActiveRecord::RecordNotFound) |
|
150 |
+ end |
|
151 |
+ end |
|
152 |
+end |
@@ -4,6 +4,7 @@ jane_website_agent: |
||
4 | 4 |
events_count: 1 |
5 | 5 |
schedule: "5pm" |
6 | 6 |
name: "ZKCD" |
7 |
+ guid: <%= SecureRandom.hex %> |
|
7 | 8 |
options: <%= { |
8 | 9 |
:url => "http://trailers.apple.com/trailers/home/rss/newtrailers.rss", |
9 | 10 |
:expected_update_period_in_days => 2, |
@@ -20,6 +21,7 @@ bob_website_agent: |
||
20 | 21 |
events_count: 1 |
21 | 22 |
schedule: "midnight" |
22 | 23 |
name: "ZKCD" |
24 |
+ guid: <%= SecureRandom.hex %> |
|
23 | 25 |
options: <%= { |
24 | 26 |
:url => "http://xkcd.com", |
25 | 27 |
:expected_update_period_in_days => 2, |
@@ -35,6 +37,7 @@ bob_weather_agent: |
||
35 | 37 |
user: bob |
36 | 38 |
schedule: "midnight" |
37 | 39 |
name: "SF Weather" |
40 |
+ guid: <%= SecureRandom.hex %> |
|
38 | 41 |
keep_events_for: 45 |
39 | 42 |
options: <%= { |
40 | 43 |
:location => 94102, |
@@ -48,6 +51,7 @@ jane_weather_agent: |
||
48 | 51 |
user: jane |
49 | 52 |
schedule: "midnight" |
50 | 53 |
name: "SF Weather" |
54 |
+ guid: <%= SecureRandom.hex %> |
|
51 | 55 |
keep_events_for: 30 |
52 | 56 |
options: <%= { |
53 | 57 |
:location => 94103, |
@@ -60,6 +64,7 @@ jane_rain_notifier_agent: |
||
60 | 64 |
type: Agents::TriggerAgent |
61 | 65 |
user: jane |
62 | 66 |
name: "Jane's Rain Watcher" |
67 |
+ guid: <%= SecureRandom.hex %> |
|
63 | 68 |
options: <%= { |
64 | 69 |
:expected_receive_period_in_days => "2", |
65 | 70 |
:rules => [{ |
@@ -74,6 +79,7 @@ bob_rain_notifier_agent: |
||
74 | 79 |
type: Agents::TriggerAgent |
75 | 80 |
user: bob |
76 | 81 |
name: "Bob's Rain Watcher" |
82 |
+ guid: <%= SecureRandom.hex %> |
|
77 | 83 |
options: <%= { |
78 | 84 |
:expected_receive_period_in_days => "2", |
79 | 85 |
:rules => [{ |
@@ -88,6 +94,7 @@ bob_twitter_user_agent: |
||
88 | 94 |
type: Agents::TwitterUserAgent |
89 | 95 |
user: bob |
90 | 96 |
name: "Bob's Twitter User Watcher" |
97 |
+ guid: <%= SecureRandom.hex %> |
|
91 | 98 |
options: <%= { |
92 | 99 |
:username => "tectonic", |
93 | 100 |
:expected_update_period_in_days => "2", |
@@ -101,3 +108,4 @@ bob_manual_event_agent: |
||
101 | 108 |
type: Agents::ManualEventAgent |
102 | 109 |
user: bob |
103 | 110 |
name: "Bob's event testing agent" |
111 |
+ guid: <%= SecureRandom.hex %> |
@@ -0,0 +1,15 @@ |
||
1 |
+jane_weather_agent_scenario_membership: |
|
2 |
+ agent: jane_weather_agent |
|
3 |
+ scenario: jane_weather |
|
4 |
+ |
|
5 |
+jane_rain_notifier_agent_scenario_membership: |
|
6 |
+ agent: jane_rain_notifier_agent |
|
7 |
+ scenario: jane_weather |
|
8 |
+ |
|
9 |
+bob_weather_agent_scenario_membership: |
|
10 |
+ agent: bob_weather_agent |
|
11 |
+ scenario: bob_weather |
|
12 |
+ |
|
13 |
+bob_rain_notifier_agent_scenario_membership: |
|
14 |
+ agent: bob_rain_notifier_agent |
|
15 |
+ scenario: bob_weather |
@@ -0,0 +1,13 @@ |
||
1 |
+jane_weather: |
|
2 |
+ name: Jane's weather alert Scenario |
|
3 |
+ user: jane |
|
4 |
+ description: Jane's weather alert system |
|
5 |
+ public: false |
|
6 |
+ guid: random-guid-generated-by-bob |
|
7 |
+ |
|
8 |
+bob_weather: |
|
9 |
+ name: Bob's weather alert Scenario |
|
10 |
+ user: bob |
|
11 |
+ description: Bob's weather alert system |
|
12 |
+ public: false |
|
13 |
+ guid: random-guid-generated-by-jane |
@@ -4,8 +4,10 @@ bob: |
||
4 | 4 |
email: "bob@example.com" |
5 | 5 |
username: bob |
6 | 6 |
invitation_code: <%= User::INVITATION_CODES.last %> |
7 |
+ scenario_count: 1 |
|
7 | 8 |
|
8 | 9 |
jane: |
9 | 10 |
email: "jane@example.com" |
10 | 11 |
username: jane |
11 |
- invitation_code: <%= User::INVITATION_CODES.last %> |
|
12 |
+ invitation_code: <%= User::INVITATION_CODES.last %> |
|
13 |
+ scenario_count: 1 |
@@ -0,0 +1,61 @@ |
||
1 |
+# encoding: utf-8 |
|
2 |
+ |
|
3 |
+require 'spec_helper' |
|
4 |
+ |
|
5 |
+describe AgentsExporter do |
|
6 |
+ describe "#as_json" do |
|
7 |
+ let(:name) { "My set of Agents" } |
|
8 |
+ let(:description) { "These Agents work together nicely!" } |
|
9 |
+ let(:guid) { "some-guid" } |
|
10 |
+ let(:source_url) { "http://yourhuginn.com/scenarios/2/export.json" } |
|
11 |
+ let(:agent_list) { [agents(:jane_weather_agent), agents(:jane_rain_notifier_agent)] } |
|
12 |
+ let(:exporter) { AgentsExporter.new(:agents => agent_list, :name => name, :description => description, :source_url => source_url, :guid => guid) } |
|
13 |
+ |
|
14 |
+ it "outputs a structure containing name, description, the date, all agents & their links" do |
|
15 |
+ data = exporter.as_json |
|
16 |
+ data[:name].should == name |
|
17 |
+ data[:description].should == description |
|
18 |
+ data[:source_url].should == source_url |
|
19 |
+ data[:guid].should == guid |
|
20 |
+ Time.parse(data[:exported_at]).should be_within(2).of(Time.now.utc) |
|
21 |
+ data[:links].should == [{ :source => 0, :receiver => 1 }] |
|
22 |
+ data[:agents].should == agent_list.map { |agent| exporter.agent_as_json(agent) } |
|
23 |
+ data[:agents].all? { |agent_json| agent_json[:guid].present? && agent_json[:type].present? && agent_json[:name].present? }.should be_true |
|
24 |
+ |
|
25 |
+ data[:agents][0].should_not have_key(:propagate_immediately) # can't receive events |
|
26 |
+ data[:agents][1].should_not have_key(:schedule) # can't be scheduled |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ it "does not output links to other agents outside of the incoming set" do |
|
30 |
+ Link.create!(:source_id => agents(:jane_weather_agent).id, :receiver_id => agents(:jane_website_agent).id) |
|
31 |
+ Link.create!(:source_id => agents(:jane_website_agent).id, :receiver_id => agents(:jane_rain_notifier_agent).id) |
|
32 |
+ |
|
33 |
+ exporter.as_json[:links].should == [{ :source => 0, :receiver => 1 }] |
|
34 |
+ end |
|
35 |
+ end |
|
36 |
+ |
|
37 |
+ describe "#filename" do |
|
38 |
+ it "strips special characters" do |
|
39 |
+ AgentsExporter.new(:name => "ƏfooƐƕƺbar").filename.should == "foo-bar.json" |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ it "strips punctuation" do |
|
43 |
+ AgentsExporter.new(:name => "foo,bar").filename.should == "foo-bar.json" |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ it "strips leading and trailing dashes" do |
|
47 |
+ AgentsExporter.new(:name => ",foo,").filename.should == "foo.json" |
|
48 |
+ end |
|
49 |
+ |
|
50 |
+ it "has a default when options[:name] is nil" do |
|
51 |
+ AgentsExporter.new(:name => nil).filename.should == "exported-agents.json" |
|
52 |
+ end |
|
53 |
+ |
|
54 |
+ it "has a default when the result is empty" do |
|
55 |
+ AgentsExporter.new(:name => "").filename.should == "exported-agents.json" |
|
56 |
+ AgentsExporter.new(:name => "Ə").filename.should == "exported-agents.json" |
|
57 |
+ AgentsExporter.new(:name => "-").filename.should == "exported-agents.json" |
|
58 |
+ AgentsExporter.new(:name => ",,").filename.should == "exported-agents.json" |
|
59 |
+ end |
|
60 |
+ end |
|
61 |
+end |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/working_helpers' |
|
3 | 2 |
|
4 | 3 |
describe Agent do |
5 | 4 |
it_behaves_like WorkingHelpers |
@@ -122,6 +121,14 @@ describe Agent do |
||
122 | 121 |
stub(Agents::CannotBeScheduled).valid_type?("Agents::CannotBeScheduled") { true } |
123 | 122 |
end |
124 | 123 |
|
124 |
+ let(:new_instance) do |
|
125 |
+ agent = Agents::SomethingSource.new(:name => "some agent") |
|
126 |
+ agent.user = users(:bob) |
|
127 |
+ agent |
|
128 |
+ end |
|
129 |
+ |
|
130 |
+ it_behaves_like HasGuid |
|
131 |
+ |
|
125 | 132 |
describe ".default_schedule" do |
126 | 133 |
it "stores the default on the class" do |
127 | 134 |
Agents::SomethingSource.default_schedule.should == "2pm" |
@@ -480,6 +487,23 @@ describe Agent do |
||
480 | 487 |
agent.should have(0).errors_on(:sources) |
481 | 488 |
end |
482 | 489 |
|
490 |
+ it "should not allow scenarios owned by other people" do |
|
491 |
+ agent = Agents::SomethingSource.new(:name => "something") |
|
492 |
+ agent.user = users(:bob) |
|
493 |
+ |
|
494 |
+ agent.scenario_ids = [scenarios(:bob_weather).id] |
|
495 |
+ agent.should have(0).errors_on(:scenarios) |
|
496 |
+ |
|
497 |
+ agent.scenario_ids = [scenarios(:bob_weather).id, scenarios(:jane_weather).id] |
|
498 |
+ agent.should have(1).errors_on(:scenarios) |
|
499 |
+ |
|
500 |
+ agent.scenario_ids = [scenarios(:jane_weather).id] |
|
501 |
+ agent.should have(1).errors_on(:scenarios) |
|
502 |
+ |
|
503 |
+ agent.user = users(:jane) |
|
504 |
+ agent.should have(0).errors_on(:scenarios) |
|
505 |
+ end |
|
506 |
+ |
|
483 | 507 |
it "validates keep_events_for" do |
484 | 508 |
agent = Agents::SomethingSource.new(:name => "something") |
485 | 509 |
agent.user = users(:bob) |
@@ -1,7 +1,6 @@ |
||
1 | 1 |
# encoding: utf-8 |
2 | 2 |
|
3 | 3 |
require 'spec_helper' |
4 |
-require 'models/concerns/liquid_interpolatable' |
|
5 | 4 |
|
6 | 5 |
describe Agents::DataOutputAgent do |
7 | 6 |
it_behaves_like LiquidInterpolatable |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::HipchatAgent do |
5 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::HumanTaskAgent do |
5 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::JabberAgent do |
5 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::PeakDetectorAgent do |
5 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::PushbulletAgent do |
5 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::SlackAgent do |
5 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -51,7 +50,8 @@ describe Agents::SlackAgent do |
||
51 | 50 |
username: @event.payload[:username] |
52 | 51 |
) |
53 | 52 |
end |
54 |
- expect(@checker.receive([@event])).to_not raise_error |
|
53 |
+ |
|
54 |
+ lambda { @checker.receive([@event]) }.should_not raise_error |
|
55 | 55 |
end |
56 | 56 |
end |
57 | 57 |
|
@@ -1,6 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 |
- |
|
4 | 2 |
|
5 | 3 |
describe Agents::TranslationAgent do |
6 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::TriggerAgent do |
5 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -0,0 +1,411 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe ScenarioImport do |
|
4 |
+ let(:user) { users(:bob) } |
|
5 |
+ let(:guid) { "somescenarioguid" } |
|
6 |
+ let(:description) { "This is a cool Huginn Scenario that does something useful!" } |
|
7 |
+ let(:name) { "A useful Scenario" } |
|
8 |
+ let(:source_url) { "http://example.com/scenarios/2/export.json" } |
|
9 |
+ let(:weather_agent_options) { |
|
10 |
+ { |
|
11 |
+ 'api_key' => 'some-api-key', |
|
12 |
+ 'location' => '12345' |
|
13 |
+ } |
|
14 |
+ } |
|
15 |
+ let(:trigger_agent_options) { |
|
16 |
+ { |
|
17 |
+ 'expected_receive_period_in_days' => 2, |
|
18 |
+ 'rules' => [{ |
|
19 |
+ 'type' => "regex", |
|
20 |
+ 'value' => "rain|storm", |
|
21 |
+ 'path' => "conditions", |
|
22 |
+ }], |
|
23 |
+ 'message' => "Looks like rain!" |
|
24 |
+ } |
|
25 |
+ } |
|
26 |
+ let(:valid_parsed_weather_agent_data) do |
|
27 |
+ { |
|
28 |
+ :type => "Agents::WeatherAgent", |
|
29 |
+ :name => "a weather agent", |
|
30 |
+ :schedule => "5pm", |
|
31 |
+ :keep_events_for => 14, |
|
32 |
+ :disabled => true, |
|
33 |
+ :guid => "a-weather-agent", |
|
34 |
+ :options => weather_agent_options |
|
35 |
+ } |
|
36 |
+ end |
|
37 |
+ let(:valid_parsed_trigger_agent_data) do |
|
38 |
+ { |
|
39 |
+ :type => "Agents::TriggerAgent", |
|
40 |
+ :name => "listen for weather", |
|
41 |
+ :keep_events_for => 0, |
|
42 |
+ :propagate_immediately => true, |
|
43 |
+ :disabled => false, |
|
44 |
+ :guid => "a-trigger-agent", |
|
45 |
+ :options => trigger_agent_options |
|
46 |
+ } |
|
47 |
+ end |
|
48 |
+ let(:valid_parsed_data) do |
|
49 |
+ { |
|
50 |
+ :name => name, |
|
51 |
+ :description => description, |
|
52 |
+ :guid => guid, |
|
53 |
+ :source_url => source_url, |
|
54 |
+ :exported_at => 2.days.ago.utc.iso8601, |
|
55 |
+ :agents => [ |
|
56 |
+ valid_parsed_weather_agent_data, |
|
57 |
+ valid_parsed_trigger_agent_data |
|
58 |
+ ], |
|
59 |
+ :links => [ |
|
60 |
+ { :source => 0, :receiver => 1 } |
|
61 |
+ ] |
|
62 |
+ } |
|
63 |
+ end |
|
64 |
+ let(:valid_data) { valid_parsed_data.to_json } |
|
65 |
+ let(:invalid_data) { { :name => "some scenario missing a guid" }.to_json } |
|
66 |
+ |
|
67 |
+ describe "initialization" do |
|
68 |
+ it "is initialized with an attributes hash" do |
|
69 |
+ ScenarioImport.new(:url => "http://google.com").url.should == "http://google.com" |
|
70 |
+ end |
|
71 |
+ end |
|
72 |
+ |
|
73 |
+ describe "validations" do |
|
74 |
+ subject do |
|
75 |
+ _import = ScenarioImport.new |
|
76 |
+ _import.set_user(user) |
|
77 |
+ _import |
|
78 |
+ end |
|
79 |
+ |
|
80 |
+ it "is not valid when none of file, url, or data are present" do |
|
81 |
+ subject.should_not be_valid |
|
82 |
+ subject.should have(1).error_on(:base) |
|
83 |
+ subject.errors[:base].should include("Please provide either a Scenario JSON File or a Public Scenario URL.") |
|
84 |
+ end |
|
85 |
+ |
|
86 |
+ describe "data" do |
|
87 |
+ it "should be invalid with invalid data" do |
|
88 |
+ subject.data = invalid_data |
|
89 |
+ subject.should_not be_valid |
|
90 |
+ subject.should have(1).error_on(:base) |
|
91 |
+ |
|
92 |
+ subject.data = "foo" |
|
93 |
+ subject.should_not be_valid |
|
94 |
+ subject.should have(1).error_on(:base) |
|
95 |
+ |
|
96 |
+ # It also clears the data when invalid |
|
97 |
+ subject.data.should be_nil |
|
98 |
+ end |
|
99 |
+ |
|
100 |
+ it "should be valid with valid data" do |
|
101 |
+ subject.data = valid_data |
|
102 |
+ subject.should be_valid |
|
103 |
+ end |
|
104 |
+ end |
|
105 |
+ |
|
106 |
+ describe "url" do |
|
107 |
+ it "should be invalid with an unreasonable URL" do |
|
108 |
+ subject.url = "foo" |
|
109 |
+ subject.should_not be_valid |
|
110 |
+ subject.should have(1).error_on(:url) |
|
111 |
+ subject.errors[:url].should include("appears to be invalid") |
|
112 |
+ end |
|
113 |
+ |
|
114 |
+ it "should be invalid when the referenced url doesn't contain a scenario" do |
|
115 |
+ stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => invalid_data) |
|
116 |
+ subject.url = "http://example.com/scenarios/1/export.json" |
|
117 |
+ subject.should_not be_valid |
|
118 |
+ subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") |
|
119 |
+ end |
|
120 |
+ |
|
121 |
+ it "should be valid when the url points to a valid scenario" do |
|
122 |
+ stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => valid_data) |
|
123 |
+ subject.url = "http://example.com/scenarios/1/export.json" |
|
124 |
+ subject.should be_valid |
|
125 |
+ end |
|
126 |
+ end |
|
127 |
+ |
|
128 |
+ describe "file" do |
|
129 |
+ it "should be invalid when the uploaded file doesn't contain a scenario" do |
|
130 |
+ subject.file = StringIO.new("foo") |
|
131 |
+ subject.should_not be_valid |
|
132 |
+ subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") |
|
133 |
+ |
|
134 |
+ subject.file = StringIO.new(invalid_data) |
|
135 |
+ subject.should_not be_valid |
|
136 |
+ subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") |
|
137 |
+ end |
|
138 |
+ |
|
139 |
+ it "should be valid with a valid uploaded scenario" do |
|
140 |
+ subject.file = StringIO.new(valid_data) |
|
141 |
+ subject.should be_valid |
|
142 |
+ end |
|
143 |
+ end |
|
144 |
+ end |
|
145 |
+ |
|
146 |
+ describe "#dangerous?" do |
|
147 |
+ it "returns false on most Agents" do |
|
148 |
+ ScenarioImport.new(:data => valid_data).should_not be_dangerous |
|
149 |
+ end |
|
150 |
+ |
|
151 |
+ it "returns true if a ShellCommandAgent is present" do |
|
152 |
+ valid_parsed_data[:agents][0][:type] = "Agents::ShellCommandAgent" |
|
153 |
+ ScenarioImport.new(:data => valid_parsed_data.to_json).should be_dangerous |
|
154 |
+ end |
|
155 |
+ end |
|
156 |
+ |
|
157 |
+ describe "#import and #generate_diff" do |
|
158 |
+ let(:scenario_import) do |
|
159 |
+ _import = ScenarioImport.new(:data => valid_data) |
|
160 |
+ _import.set_user users(:bob) |
|
161 |
+ _import |
|
162 |
+ end |
|
163 |
+ |
|
164 |
+ context "when this scenario has never been seen before" do |
|
165 |
+ describe "#import" do |
|
166 |
+ it "makes a new scenario" do |
|
167 |
+ lambda { |
|
168 |
+ scenario_import.import(:skip_agents => true) |
|
169 |
+ }.should change { users(:bob).scenarios.count }.by(1) |
|
170 |
+ |
|
171 |
+ scenario_import.scenario.name.should == name |
|
172 |
+ scenario_import.scenario.description.should == description |
|
173 |
+ scenario_import.scenario.guid.should == guid |
|
174 |
+ scenario_import.scenario.source_url.should == source_url |
|
175 |
+ scenario_import.scenario.public.should be_false |
|
176 |
+ end |
|
177 |
+ |
|
178 |
+ it "creates the Agents" do |
|
179 |
+ lambda { |
|
180 |
+ scenario_import.import |
|
181 |
+ }.should change { users(:bob).agents.count }.by(2) |
|
182 |
+ |
|
183 |
+ weather_agent = scenario_import.scenario.agents.find_by(:guid => "a-weather-agent") |
|
184 |
+ trigger_agent = scenario_import.scenario.agents.find_by(:guid => "a-trigger-agent") |
|
185 |
+ |
|
186 |
+ weather_agent.name.should == "a weather agent" |
|
187 |
+ weather_agent.schedule.should == "5pm" |
|
188 |
+ weather_agent.keep_events_for.should == 14 |
|
189 |
+ weather_agent.propagate_immediately.should be_false |
|
190 |
+ weather_agent.should be_disabled |
|
191 |
+ weather_agent.memory.should be_empty |
|
192 |
+ weather_agent.options.should == weather_agent_options |
|
193 |
+ |
|
194 |
+ trigger_agent.name.should == "listen for weather" |
|
195 |
+ trigger_agent.sources.should == [weather_agent] |
|
196 |
+ trigger_agent.schedule.should be_nil |
|
197 |
+ trigger_agent.keep_events_for.should == 0 |
|
198 |
+ trigger_agent.propagate_immediately.should be_true |
|
199 |
+ trigger_agent.should_not be_disabled |
|
200 |
+ trigger_agent.memory.should be_empty |
|
201 |
+ trigger_agent.options.should == trigger_agent_options |
|
202 |
+ end |
|
203 |
+ |
|
204 |
+ it "creates new Agents, even if one already exists with the given guid (so that we don't overwrite a user's work outside of the scenario)" do |
|
205 |
+ agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent" |
|
206 |
+ |
|
207 |
+ lambda { |
|
208 |
+ scenario_import.import |
|
209 |
+ }.should change { users(:bob).agents.count }.by(2) |
|
210 |
+ end |
|
211 |
+ end |
|
212 |
+ |
|
213 |
+ describe "#generate_diff" do |
|
214 |
+ it "returns AgentDiff objects for the incoming Agents" do |
|
215 |
+ scenario_import.should be_valid |
|
216 |
+ |
|
217 |
+ agent_diffs = scenario_import.agent_diffs |
|
218 |
+ |
|
219 |
+ weather_agent_diff = agent_diffs[0] |
|
220 |
+ trigger_agent_diff = agent_diffs[1] |
|
221 |
+ |
|
222 |
+ valid_parsed_weather_agent_data.each do |key, value| |
|
223 |
+ if key == :type |
|
224 |
+ value = value.split("::").last |
|
225 |
+ end |
|
226 |
+ weather_agent_diff.should respond_to(key) |
|
227 |
+ field = weather_agent_diff.send(key) |
|
228 |
+ field.should be_a(ScenarioImport::AgentDiff::FieldDiff) |
|
229 |
+ field.incoming.should == value |
|
230 |
+ field.updated.should == value |
|
231 |
+ field.current.should be_nil |
|
232 |
+ end |
|
233 |
+ weather_agent_diff.should_not respond_to(:propagate_immediately) |
|
234 |
+ |
|
235 |
+ valid_parsed_trigger_agent_data.each do |key, value| |
|
236 |
+ if key == :type |
|
237 |
+ value = value.split("::").last |
|
238 |
+ end |
|
239 |
+ trigger_agent_diff.should respond_to(key) |
|
240 |
+ field = trigger_agent_diff.send(key) |
|
241 |
+ field.should be_a(ScenarioImport::AgentDiff::FieldDiff) |
|
242 |
+ field.incoming.should == value |
|
243 |
+ field.updated.should == value |
|
244 |
+ field.current.should be_nil |
|
245 |
+ end |
|
246 |
+ trigger_agent_diff.should_not respond_to(:schedule) |
|
247 |
+ end |
|
248 |
+ end |
|
249 |
+ end |
|
250 |
+ |
|
251 |
+ context "when an a scenario already exists with the given guid" do |
|
252 |
+ let!(:existing_scenario) do |
|
253 |
+ _existing_scenerio = users(:bob).scenarios.build(:name => "an existing scenario", :description => "something") |
|
254 |
+ _existing_scenerio.guid = guid |
|
255 |
+ _existing_scenerio.save! |
|
256 |
+ |
|
257 |
+ agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent" |
|
258 |
+ agents(:bob_weather_agent).scenarios << _existing_scenerio |
|
259 |
+ |
|
260 |
+ _existing_scenerio |
|
261 |
+ end |
|
262 |
+ |
|
263 |
+ describe "#import" do |
|
264 |
+ it "uses the existing scenario, updating its data" do |
|
265 |
+ lambda { |
|
266 |
+ scenario_import.import(:skip_agents => true) |
|
267 |
+ scenario_import.scenario.should == existing_scenario |
|
268 |
+ }.should_not change { users(:bob).scenarios.count } |
|
269 |
+ |
|
270 |
+ existing_scenario.reload |
|
271 |
+ existing_scenario.guid.should == guid |
|
272 |
+ existing_scenario.description.should == description |
|
273 |
+ existing_scenario.name.should == name |
|
274 |
+ existing_scenario.source_url.should == source_url |
|
275 |
+ existing_scenario.public.should be_false |
|
276 |
+ end |
|
277 |
+ |
|
278 |
+ it "updates any existing agents in the scenario, and makes new ones as needed" do |
|
279 |
+ scenario_import.should be_valid |
|
280 |
+ |
|
281 |
+ lambda { |
|
282 |
+ scenario_import.import |
|
283 |
+ }.should change { users(:bob).agents.count }.by(1) # One, because the weather agent already existed. |
|
284 |
+ |
|
285 |
+ weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent") |
|
286 |
+ trigger_agent = existing_scenario.agents.find_by(:guid => "a-trigger-agent") |
|
287 |
+ |
|
288 |
+ weather_agent.should == agents(:bob_weather_agent) |
|
289 |
+ |
|
290 |
+ weather_agent.name.should == "a weather agent" |
|
291 |
+ weather_agent.schedule.should == "5pm" |
|
292 |
+ weather_agent.keep_events_for.should == 14 |
|
293 |
+ weather_agent.propagate_immediately.should be_false |
|
294 |
+ weather_agent.should be_disabled |
|
295 |
+ weather_agent.memory.should be_empty |
|
296 |
+ weather_agent.options.should == weather_agent_options |
|
297 |
+ |
|
298 |
+ trigger_agent.name.should == "listen for weather" |
|
299 |
+ trigger_agent.sources.should == [weather_agent] |
|
300 |
+ trigger_agent.schedule.should be_nil |
|
301 |
+ trigger_agent.keep_events_for.should == 0 |
|
302 |
+ trigger_agent.propagate_immediately.should be_true |
|
303 |
+ trigger_agent.should_not be_disabled |
|
304 |
+ trigger_agent.memory.should be_empty |
|
305 |
+ trigger_agent.options.should == trigger_agent_options |
|
306 |
+ end |
|
307 |
+ |
|
308 |
+ it "honors updates coming from the UI" do |
|
309 |
+ scenario_import.merges = { |
|
310 |
+ "0" => { |
|
311 |
+ "name" => "updated name", |
|
312 |
+ "schedule" => "6pm", |
|
313 |
+ "keep_events_for" => "2", |
|
314 |
+ "disabled" => "false", |
|
315 |
+ "options" => weather_agent_options.merge("api_key" => "foo").to_json |
|
316 |
+ } |
|
317 |
+ } |
|
318 |
+ |
|
319 |
+ scenario_import.should be_valid |
|
320 |
+ |
|
321 |
+ scenario_import.import.should be_true |
|
322 |
+ |
|
323 |
+ weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent") |
|
324 |
+ weather_agent.name.should == "updated name" |
|
325 |
+ weather_agent.schedule.should == "6pm" |
|
326 |
+ weather_agent.keep_events_for.should == 2 |
|
327 |
+ weather_agent.should_not be_disabled |
|
328 |
+ weather_agent.options.should == weather_agent_options.merge("api_key" => "foo") |
|
329 |
+ end |
|
330 |
+ |
|
331 |
+ it "adds errors when updated agents are invalid" do |
|
332 |
+ scenario_import.merges = { |
|
333 |
+ "0" => { |
|
334 |
+ "name" => "", |
|
335 |
+ "schedule" => "foo", |
|
336 |
+ "keep_events_for" => "2", |
|
337 |
+ "options" => weather_agent_options.merge("api_key" => "").to_json |
|
338 |
+ } |
|
339 |
+ } |
|
340 |
+ |
|
341 |
+ scenario_import.import.should be_false |
|
342 |
+ |
|
343 |
+ errors = scenario_import.errors.full_messages.to_sentence |
|
344 |
+ errors.should =~ /Name can't be blank/ |
|
345 |
+ errors.should =~ /api_key is required/ |
|
346 |
+ errors.should =~ /Schedule is not a valid schedule/ |
|
347 |
+ end |
|
348 |
+ end |
|
349 |
+ |
|
350 |
+ describe "#generate_diff" do |
|
351 |
+ it "returns AgentDiff objects that include 'current' values from any agents that already exist" do |
|
352 |
+ agent_diffs = scenario_import.agent_diffs |
|
353 |
+ weather_agent_diff = agent_diffs[0] |
|
354 |
+ trigger_agent_diff = agent_diffs[1] |
|
355 |
+ |
|
356 |
+ # Already exists |
|
357 |
+ weather_agent_diff.agent.should == agents(:bob_weather_agent) |
|
358 |
+ valid_parsed_weather_agent_data.each do |key, value| |
|
359 |
+ next if key == :type |
|
360 |
+ weather_agent_diff.send(key).current.should == agents(:bob_weather_agent).send(key) |
|
361 |
+ end |
|
362 |
+ |
|
363 |
+ # Doesn't exist yet |
|
364 |
+ valid_parsed_trigger_agent_data.each do |key, value| |
|
365 |
+ trigger_agent_diff.send(key).current.should be_nil |
|
366 |
+ end |
|
367 |
+ end |
|
368 |
+ |
|
369 |
+ it "sets the 'updated' FieldDiff values based on any feedback from the user" do |
|
370 |
+ scenario_import.merges = { |
|
371 |
+ "0" => { |
|
372 |
+ "name" => "a new name", |
|
373 |
+ "schedule" => "6pm", |
|
374 |
+ "keep_events_for" => "2", |
|
375 |
+ "disabled" => "true", |
|
376 |
+ "options" => weather_agent_options.merge("api_key" => "foo").to_json |
|
377 |
+ }, |
|
378 |
+ "1" => { |
|
379 |
+ "name" => "another new name" |
|
380 |
+ } |
|
381 |
+ } |
|
382 |
+ |
|
383 |
+ scenario_import.should be_valid |
|
384 |
+ |
|
385 |
+ agent_diffs = scenario_import.agent_diffs |
|
386 |
+ weather_agent_diff = agent_diffs[0] |
|
387 |
+ trigger_agent_diff = agent_diffs[1] |
|
388 |
+ |
|
389 |
+ weather_agent_diff.name.current.should == agents(:bob_weather_agent).name |
|
390 |
+ weather_agent_diff.name.incoming.should == valid_parsed_weather_agent_data[:name] |
|
391 |
+ weather_agent_diff.name.updated.should == "a new name" |
|
392 |
+ |
|
393 |
+ weather_agent_diff.schedule.updated.should == "6pm" |
|
394 |
+ weather_agent_diff.keep_events_for.updated.should == "2" |
|
395 |
+ weather_agent_diff.disabled.updated.should == "true" |
|
396 |
+ weather_agent_diff.options.updated.should == weather_agent_options.merge("api_key" => "foo") |
|
397 |
+ end |
|
398 |
+ |
|
399 |
+ it "adds errors on validation when updated options are unparsable" do |
|
400 |
+ scenario_import.merges = { |
|
401 |
+ "0" => { |
|
402 |
+ "options" => '{' |
|
403 |
+ } |
|
404 |
+ } |
|
405 |
+ scenario_import.should_not be_valid |
|
406 |
+ scenario_import.should have(1).error_on(:base) |
|
407 |
+ end |
|
408 |
+ end |
|
409 |
+ end |
|
410 |
+ end |
|
411 |
+end |
@@ -0,0 +1,43 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Scenario do |
|
4 |
+ let(:new_instance) { users(:bob).scenarios.build(:name => "some scenario") } |
|
5 |
+ |
|
6 |
+ it_behaves_like HasGuid |
|
7 |
+ |
|
8 |
+ describe "validations" do |
|
9 |
+ before do |
|
10 |
+ new_instance.should be_valid |
|
11 |
+ end |
|
12 |
+ |
|
13 |
+ it "validates the presence of name" do |
|
14 |
+ new_instance.name = '' |
|
15 |
+ new_instance.should_not be_valid |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ it "validates the presence of user" do |
|
19 |
+ new_instance.user = nil |
|
20 |
+ new_instance.should_not be_valid |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+ it "only allows Agents owned by user" do |
|
24 |
+ new_instance.agent_ids = [agents(:bob_website_agent).id] |
|
25 |
+ new_instance.should be_valid |
|
26 |
+ |
|
27 |
+ new_instance.agent_ids = [agents(:jane_website_agent).id] |
|
28 |
+ new_instance.should_not be_valid |
|
29 |
+ end |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ describe "counters" do |
|
33 |
+ it "maintains a counter cache on user" do |
|
34 |
+ lambda { |
|
35 |
+ new_instance.save! |
|
36 |
+ }.should change { users(:bob).reload.scenario_count }.by(1) |
|
37 |
+ |
|
38 |
+ lambda { |
|
39 |
+ new_instance.destroy |
|
40 |
+ }.should change { users(:bob).reload.scenario_count }.by(-1) |
|
41 |
+ end |
|
42 |
+ end |
|
43 |
+end |
@@ -0,0 +1,12 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+shared_examples_for HasGuid do |
|
4 |
+ it "gets created before_save, but only if it's not present" do |
|
5 |
+ instance = new_instance |
|
6 |
+ instance.guid.should be_nil |
|
7 |
+ instance.save! |
|
8 |
+ instance.guid.should_not be_nil |
|
9 |
+ |
|
10 |
+ lambda { instance.save! }.should_not change { instance.reload.guid } |
|
11 |
+ end |
|
12 |
+end |
@@ -3,7 +3,7 @@ require 'spec_helper' |
||
3 | 3 |
shared_examples_for WorkingHelpers do |
4 | 4 |
describe "recent_error_logs?" do |
5 | 5 |
it "returns true if last_error_log_at is near last_event_at" do |
6 |
- agent = Agent.new |
|
6 |
+ agent = described_class.new |
|
7 | 7 |
|
8 | 8 |
agent.last_error_log_at = 10.minutes.ago |
9 | 9 |
agent.last_event_at = 10.minutes.ago |
@@ -26,9 +26,10 @@ shared_examples_for WorkingHelpers do |
||
26 | 26 |
agent.recent_error_logs?.should be_false |
27 | 27 |
end |
28 | 28 |
end |
29 |
+ |
|
29 | 30 |
describe "received_event_without_error?" do |
30 | 31 |
before do |
31 |
- @agent = Agent.new |
|
32 |
+ @agent = described_class.new |
|
32 | 33 |
end |
33 | 34 |
|
34 | 35 |
it "should return false until the first event was received" do |
@@ -49,5 +50,4 @@ shared_examples_for WorkingHelpers do |
||
49 | 50 |
@agent.received_event_without_error?.should == true |
50 | 51 |
end |
51 | 52 |
end |
52 |
- |
|
53 | 53 |
end |
@@ -1,5 +1,5 @@ |
||
1 | 1 |
/* |
2 |
- Copyright (c) 2013, Andrew Cantino |
|
2 |
+ Copyright (c) 2014, Andrew Cantino |
|
3 | 3 |
Copyright (c) 2009, Andrew Cantino & Kyle Maxwell |
4 | 4 |
|
5 | 5 |
Permission is hereby granted, free of charge, to any person obtaining a copy |
@@ -23,8 +23,8 @@ |
||
23 | 23 |
|
24 | 24 |
|
25 | 25 |
|
26 |
- You will probably need to tell the editor where to find its add and delete images. In your |
|
27 |
- code before you make the editor, do something like this: |
|
26 |
+ You will probably need to tell the editor where to find its 'add' and 'delete' images. In your |
|
27 |
+ code, before you make the editor, do something like this: |
|
28 | 28 |
JSONEditor.prototype.ADD_IMG = '/javascripts/jsoneditor/add.png'; |
29 | 29 |
JSONEditor.prototype.DELETE_IMG = '/javascripts/jsoneditor/delete.png'; |
30 | 30 |
|
@@ -36,504 +36,529 @@ |
||
36 | 36 |
*/ |
37 | 37 |
|
38 | 38 |
|
39 |
-function JSONEditorBase(options) { |
|
40 |
- if (!options) options = {}; |
|
41 |
- this.builderShowing = true; |
|
42 |
- this.ADD_IMG = options.ADD_IMG || 'lib/images/add.png'; |
|
43 |
- this.DELETE_IMG = options.DELETE_IMG || 'lib/images/delete.png'; |
|
44 |
- this.functionButtonsEnabled = false; |
|
45 |
- this._doTruncation = true; |
|
46 |
- this._showWipe = options.showWipe; |
|
47 |
-} |
|
48 |
- |
|
49 |
-function JSONEditor(wrapped, width, height) { |
|
50 |
- this.history = []; |
|
51 |
- this.historyPointer = -1; |
|
52 |
- if (wrapped == null || (wrapped.get && wrapped.get(0) == null)) throw "Must provide an element to wrap."; |
|
53 |
- var width = width || 600; |
|
54 |
- var height = height || 300; |
|
55 |
- this.wrapped = $(wrapped); |
|
56 |
- |
|
57 |
- this.wrapped.wrap('<div class="json-editor"></div>'); |
|
58 |
- this.container = $(this.wrapped.parent()); |
|
59 |
- this.container.width(width).height(height); |
|
60 |
- this.wrapped.width(width).height(height); |
|
61 |
- this.wrapped.hide(); |
|
62 |
- this.container.css("position", "relative"); |
|
63 |
- this.doAutoFocus = false; |
|
64 |
- this.editingUnfocused(); |
|
65 |
- |
|
66 |
- this.rebuild(); |
|
67 |
- var self = this; |
|
68 |
- this.container.focus(function(){ |
|
69 |
- $(this).children('textarea').height(self.container.height() - self.functionButtons.height() - 5); |
|
70 |
- $(this).children('.builder').height(self.container.height() - self.functionButtons.height() - 10); |
|
71 |
- }); |
|
72 |
- |
|
73 |
- return this; |
|
74 |
-} |
|
75 |
-JSONEditor.prototype = new JSONEditorBase(); |
|
76 |
- |
|
77 |
-JSONEditor.prototype.braceUI = function(key, struct) { |
|
78 |
- var self = this; |
|
79 |
- return $('<a class="icon" href="#"><strong>{</strong></a>').click(function(e) { |
|
80 |
- struct[key] = { "??": struct[key] }; |
|
81 |
- self.doAutoFocus = true; |
|
82 |
- self.rebuild(); |
|
83 |
- return false; |
|
84 |
- }); |
|
85 |
-}; |
|
86 |
- |
|
87 |
-JSONEditor.prototype.bracketUI = function(key, struct) { |
|
88 |
- var self = this; |
|
89 |
- return $('<a class="icon" href="#"><strong>[</a>').click(function(e) { |
|
90 |
- struct[key] = [ struct[key] ]; |
|
91 |
- self.doAutoFocus = true; |
|
92 |
- self.rebuild(); |
|
93 |
- return false; |
|
94 |
- }); |
|
95 |
-}; |
|
96 |
- |
|
97 |
-JSONEditor.prototype.deleteUI = function(key, struct, fullDelete) { |
|
98 |
- var self = this; |
|
99 |
- return $('<a class="icon" href="#" title="delete"><img src="' + this.DELETE_IMG + '" border=0/></a>').click(function(e) { |
|
100 |
- if (!fullDelete) { |
|
101 |
- var didSomething = false; |
|
102 |
- if (struct[key] instanceof Array) { |
|
103 |
- if(struct[key].length > 0) { |
|
104 |
- struct[key] = struct[key][0]; |
|
105 |
- didSomething = true; |
|
39 |
+(function() { |
|
40 |
+ |
|
41 |
+ window.JSONEditor = (function() { |
|
42 |
+ |
|
43 |
+ function JSONEditor(wrapped, options) { |
|
44 |
+ if (options == null) { |
|
45 |
+ options = {}; |
|
46 |
+ } |
|
47 |
+ this.builderShowing = true; |
|
48 |
+ this.ADD_IMG || (this.ADD_IMG = options.ADD_IMG || 'lib/images/add.png'); |
|
49 |
+ this.DELETE_IMG || (this.DELETE_IMG = options.DELETE_IMG || 'lib/images/delete.png'); |
|
50 |
+ this.functionButtonsEnabled = false; |
|
51 |
+ this._doTruncation = true; |
|
52 |
+ this._showWipe = options.showWipe; |
|
53 |
+ this.history = []; |
|
54 |
+ this.historyPointer = -1; |
|
55 |
+ if (wrapped === null || (wrapped.get && wrapped.get(0) === null)) { |
|
56 |
+ throw "Must provide an element to wrap."; |
|
57 |
+ } |
|
58 |
+ this.wrapped = $(wrapped); |
|
59 |
+ this.wrapped.wrap('<div class="json-editor"></div>'); |
|
60 |
+ this.container = $(this.wrapped.parent()); |
|
61 |
+ this.wrapped.hide(); |
|
62 |
+ this.container.css("position", "relative"); |
|
63 |
+ this.doAutoFocus = false; |
|
64 |
+ this.editingUnfocused(); |
|
65 |
+ this.rebuild(); |
|
66 |
+ } |
|
67 |
+ |
|
68 |
+ JSONEditor.prototype.braceUI = function(key, struct) { |
|
69 |
+ var _this = this; |
|
70 |
+ return $('<a class="icon" href="#"><strong>{</strong></a>').click(function(e) { |
|
71 |
+ e.preventDefault(); |
|
72 |
+ struct[key] = { |
|
73 |
+ "??": struct[key] |
|
74 |
+ }; |
|
75 |
+ _this.doAutoFocus = true; |
|
76 |
+ return _this.rebuild(); |
|
77 |
+ }); |
|
78 |
+ }; |
|
79 |
+ |
|
80 |
+ JSONEditor.prototype.bracketUI = function(key, struct) { |
|
81 |
+ var _this = this; |
|
82 |
+ return $('<a class="icon" href="#"><strong>[</a>').click(function(e) { |
|
83 |
+ e.preventDefault(); |
|
84 |
+ struct[key] = [struct[key]]; |
|
85 |
+ _this.doAutoFocus = true; |
|
86 |
+ return _this.rebuild(); |
|
87 |
+ }); |
|
88 |
+ }; |
|
89 |
+ |
|
90 |
+ JSONEditor.prototype.deleteUI = function(key, struct, fullDelete) { |
|
91 |
+ var _this = this; |
|
92 |
+ return $("<a class='icon' href='#' title='delete'><img src='" + this.DELETE_IMG + "' border=0 /></a>").click(function(e) { |
|
93 |
+ var didSomething, subkey, subval, _ref; |
|
94 |
+ e.preventDefault(); |
|
95 |
+ if (!fullDelete) { |
|
96 |
+ didSomething = false; |
|
97 |
+ if (struct[key] instanceof Array) { |
|
98 |
+ if (struct[key].length > 0) { |
|
99 |
+ struct[key] = struct[key][0]; |
|
100 |
+ didSomething = true; |
|
101 |
+ } |
|
102 |
+ } else if (struct[key] instanceof Object) { |
|
103 |
+ _ref = struct[key]; |
|
104 |
+ for (subkey in _ref) { |
|
105 |
+ subval = _ref[subkey]; |
|
106 |
+ struct[key] = struct[key][subkey]; |
|
107 |
+ didSomething = true; |
|
108 |
+ break; |
|
109 |
+ } |
|
110 |
+ } |
|
111 |
+ if (didSomething) { |
|
112 |
+ _this.rebuild(); |
|
113 |
+ return; |
|
114 |
+ } |
|
106 | 115 |
} |
107 |
- } else if (struct[key] instanceof Object) { |
|
108 |
- for (var i in struct[key]) { |
|
109 |
- struct[key] = struct[key][i]; |
|
110 |
- didSomething = true; |
|
111 |
- break; |
|
116 |
+ if (struct instanceof Array) { |
|
117 |
+ struct.splice(key, 1); |
|
118 |
+ } else { |
|
119 |
+ delete struct[key]; |
|
120 |
+ } |
|
121 |
+ return _this.rebuild(); |
|
122 |
+ }); |
|
123 |
+ }; |
|
124 |
+ |
|
125 |
+ JSONEditor.prototype.wipeUI = function(key, struct) { |
|
126 |
+ var _this = this; |
|
127 |
+ return $('<a class="icon" href="#" title="wipe"><strong>W</strong></a>').click(function(e) { |
|
128 |
+ e.preventDefault(); |
|
129 |
+ if (struct instanceof Array) { |
|
130 |
+ struct.splice(key, 1); |
|
131 |
+ } else { |
|
132 |
+ delete struct[key]; |
|
133 |
+ } |
|
134 |
+ return _this.rebuild(); |
|
135 |
+ }); |
|
136 |
+ }; |
|
137 |
+ |
|
138 |
+ JSONEditor.prototype.addUI = function(struct) { |
|
139 |
+ var _this = this; |
|
140 |
+ return $("<a class='icon' href='#' title='add'><img src='" + this.ADD_IMG + "' border=0/></a>").click(function(e) { |
|
141 |
+ e.preventDefault(); |
|
142 |
+ if (struct instanceof Array) { |
|
143 |
+ struct.push('??'); |
|
144 |
+ } else { |
|
145 |
+ struct['??'] = '??'; |
|
146 |
+ } |
|
147 |
+ _this.doAutoFocus = true; |
|
148 |
+ return _this.rebuild(); |
|
149 |
+ }); |
|
150 |
+ }; |
|
151 |
+ |
|
152 |
+ JSONEditor.prototype.undo = function() { |
|
153 |
+ if (this.saveStateIfTextChanged()) { |
|
154 |
+ if (this.historyPointer > 0) { |
|
155 |
+ this.historyPointer -= 1; |
|
156 |
+ } |
|
157 |
+ return this.restore(); |
|
158 |
+ } |
|
159 |
+ }; |
|
160 |
+ |
|
161 |
+ JSONEditor.prototype.redo = function() { |
|
162 |
+ if (this.historyPointer + 1 < this.history.length) { |
|
163 |
+ if (this.saveStateIfTextChanged()) { |
|
164 |
+ this.historyPointer += 1; |
|
165 |
+ return this.restore(); |
|
112 | 166 |
} |
113 | 167 |
} |
114 |
- if (didSomething) { |
|
115 |
- self.rebuild(); |
|
168 |
+ }; |
|
169 |
+ |
|
170 |
+ JSONEditor.prototype.showBuilder = function() { |
|
171 |
+ if (this.checkJsonInText()) { |
|
172 |
+ this.setJsonFromText(); |
|
173 |
+ this.rebuild(); |
|
174 |
+ this.wrapped.hide(); |
|
175 |
+ this.builder.show(); |
|
176 |
+ return true; |
|
177 |
+ } else { |
|
178 |
+ alert("Sorry, there appears to be an error in your JSON input. Please fix it before continuing."); |
|
116 | 179 |
return false; |
117 | 180 |
} |
118 |
- } |
|
119 |
- if (struct instanceof Array) { |
|
120 |
- struct.splice(key, 1); |
|
121 |
- } else { |
|
122 |
- delete struct[key]; |
|
123 |
- } |
|
124 |
- self.rebuild(); |
|
125 |
- return false; |
|
126 |
- }); |
|
127 |
-}; |
|
128 |
- |
|
129 |
-JSONEditor.prototype.wipeUI = function(key, struct) { |
|
130 |
- var self = this; |
|
131 |
- return $('<a class="icon" href="#" title="wipe"><strong>W</strong></a>').click(function(e) { |
|
132 |
- if (struct instanceof Array) { |
|
133 |
- struct.splice(key, 1); |
|
134 |
- } else { |
|
135 |
- delete struct[key]; |
|
136 |
- } |
|
137 |
- self.rebuild(); |
|
138 |
- return false; |
|
139 |
- }); |
|
140 |
-}; |
|
141 |
- |
|
142 |
-JSONEditor.prototype.addUI = function(struct) { |
|
143 |
- var self = this; |
|
144 |
- return $('<a class="icon" href="#" title="add"><img src="' + this.ADD_IMG + '" border=0/></a>').click(function(e) { |
|
145 |
- if (struct instanceof Array) { |
|
146 |
- struct.push('??'); |
|
147 |
- } else { |
|
148 |
- struct['??'] = '??'; |
|
149 |
- } |
|
150 |
- self.doAutoFocus = true; |
|
151 |
- self.rebuild(); |
|
152 |
- return false; |
|
153 |
- }); |
|
154 |
-}; |
|
155 |
- |
|
156 |
-JSONEditor.prototype.undo = function() { |
|
157 |
- if (this.saveStateIfTextChanged()) { |
|
158 |
- if (this.historyPointer > 0) this.historyPointer -= 1; |
|
159 |
- this.restore(); |
|
160 |
- } |
|
161 |
-}; |
|
162 |
- |
|
163 |
-JSONEditor.prototype.redo = function() { |
|
164 |
- if (this.historyPointer + 1 < this.history.length) { |
|
165 |
- if (this.saveStateIfTextChanged()) { |
|
166 |
- this.historyPointer += 1; |
|
167 |
- this.restore(); |
|
168 |
- } |
|
169 |
- } |
|
170 |
-}; |
|
171 |
- |
|
172 |
-JSONEditor.prototype.showBuilder = function() { |
|
173 |
- if (this.checkJsonInText()) { |
|
174 |
- this.setJsonFromText(); |
|
175 |
- this.rebuild(); |
|
176 |
- this.wrapped.hide(); |
|
177 |
- this.builder.show(); |
|
178 |
- return true; |
|
179 |
- } else { |
|
180 |
- alert("Sorry, there appears to be an error in your JSON input. Please fix it before continuing."); |
|
181 |
- return false; |
|
182 |
- } |
|
183 |
-}; |
|
184 |
- |
|
185 |
-JSONEditor.prototype.showText = function() { |
|
186 |
- this.builder.hide(); |
|
187 |
- this.wrapped.show(); |
|
188 |
-}; |
|
189 |
- |
|
190 |
-JSONEditor.prototype.toggleBuilder = function() { |
|
191 |
- if(this.builderShowing){ |
|
192 |
- this.showText(); |
|
193 |
- this.builderShowing = !this.builderShowing; |
|
194 |
- } else { |
|
195 |
- if (this.showBuilder()) { |
|
196 |
- this.builderShowing = !this.builderShowing; |
|
181 |
+ }; |
|
182 |
+ |
|
183 |
+ JSONEditor.prototype.showText = function() { |
|
184 |
+ this.builder.hide(); |
|
185 |
+ return this.wrapped.show(); |
|
186 |
+ }; |
|
187 |
+ |
|
188 |
+ JSONEditor.prototype.toggleBuilder = function() { |
|
189 |
+ if (this.builderShowing) { |
|
190 |
+ this.showText(); |
|
191 |
+ return this.builderShowing = !this.builderShowing; |
|
192 |
+ } else { |
|
193 |
+ if (this.showBuilder()) { |
|
194 |
+ return this.builderShowing = !this.builderShowing; |
|
195 |
+ } |
|
197 | 196 |
} |
198 |
- } |
|
199 |
-}; |
|
200 |
- |
|
201 |
-JSONEditor.prototype.showFunctionButtons = function(insider) { |
|
202 |
- if (!insider) this.functionButtonsEnabled = true; |
|
203 |
- if (this.functionButtonsEnabled) if (!this.functionButtons) { |
|
204 |
- this.functionButtons = $('<div class="function_buttons"></div>'); |
|
205 |
- var self = this; |
|
206 |
- this.functionButtons.append($('<a href="#" style="padding-right: 10px;"></a>').click(function() { |
|
207 |
- self.undo(); |
|
208 |
- return false; |
|
209 |
- }).text('Undo')).append($('<a href="#" style="padding-right: 10px;"></a>').click(function() { |
|
210 |
- self.redo(); |
|
211 |
- return false; |
|
212 |
- }).text('Redo')).append($('<a id="toggle_view" href="#" style="padding-right: 10px;"></a>').click(function() { |
|
213 |
- self.toggleBuilder(); |
|
214 |
- return false; |
|
215 |
- }).text('Toggle View')); |
|
216 |
- this.container.prepend(this.functionButtons); |
|
217 |
- this.container.height(this.container.height() + this.functionButtons.height() + 5); |
|
218 |
- } |
|
219 |
- if (this.functionButtons) { |
|
220 |
- this.wrapped.css('top', this.functionButtons.height() + 5 + 'px'); |
|
221 |
- this.builder.css('top', this.functionButtons.height() + 5 + 'px'); |
|
222 |
- } |
|
223 |
-}; |
|
224 |
- |
|
225 |
-JSONEditor.prototype.saveStateIfTextChanged = function() { |
|
226 |
- if (JSON.stringify(this.json, null, 2) != this.wrapped.get(0).value) { |
|
227 |
- if (this.checkJsonInText()) { |
|
228 |
- this.saveState(true); |
|
229 |
- } else { |
|
230 |
- if (confirm("The current JSON is malformed. If you continue, the current JSON will not be saved. Do you wish to continue?")) { |
|
231 |
- this.historyPointer += 1; |
|
197 |
+ }; |
|
198 |
+ |
|
199 |
+ JSONEditor.prototype.showFunctionButtons = function(insider) { |
|
200 |
+ var _this = this; |
|
201 |
+ if (!insider) { |
|
202 |
+ this.functionButtonsEnabled = true; |
|
203 |
+ } |
|
204 |
+ if (this.functionButtonsEnabled && !this.functionButtons) { |
|
205 |
+ this.functionButtons = $('<div class="function_buttons"></div>'); |
|
206 |
+ this.functionButtons.append($('<a href="#" style="padding-right: 10px;">Undo</a>').click(function(e) { |
|
207 |
+ e.preventDefault(); |
|
208 |
+ return _this.undo(); |
|
209 |
+ })); |
|
210 |
+ this.functionButtons.append($('<a href="#" style="padding-right: 10px;">Redo</a>').click(function(e) { |
|
211 |
+ e.preventDefault(); |
|
212 |
+ return _this.redo(); |
|
213 |
+ })); |
|
214 |
+ this.functionButtons.append($('<a id="toggle_view" href="#" style="padding-right: 10px; float: right;">Toggle View</a>').click(function(e) { |
|
215 |
+ e.preventDefault(); |
|
216 |
+ return _this.toggleBuilder(); |
|
217 |
+ })); |
|
218 |
+ return this.container.prepend(this.functionButtons); |
|
219 |
+ } |
|
220 |
+ }; |
|
221 |
+ |
|
222 |
+ JSONEditor.prototype.saveStateIfTextChanged = function() { |
|
223 |
+ if (JSON.stringify(this.json, null, 2) !== this.wrapped.get(0).value) { |
|
224 |
+ if (this.checkJsonInText()) { |
|
225 |
+ this.saveState(true); |
|
226 |
+ } else { |
|
227 |
+ if (confirm("The current JSON is malformed. If you continue, the current JSON will not be saved. Do you wish to continue?")) { |
|
228 |
+ this.historyPointer += 1; |
|
229 |
+ true; |
|
230 |
+ } else { |
|
231 |
+ false; |
|
232 |
+ } |
|
233 |
+ } |
|
234 |
+ } |
|
235 |
+ return true; |
|
236 |
+ }; |
|
237 |
+ |
|
238 |
+ JSONEditor.prototype.restore = function() { |
|
239 |
+ if (this.history[this.historyPointer]) { |
|
240 |
+ this.wrapped.get(0).value = this.history[this.historyPointer]; |
|
241 |
+ return this.rebuild(true); |
|
242 |
+ } |
|
243 |
+ }; |
|
244 |
+ |
|
245 |
+ JSONEditor.prototype.saveState = function(skipStoreText) { |
|
246 |
+ var text; |
|
247 |
+ if (this.json) { |
|
248 |
+ if (!skipStoreText) { |
|
249 |
+ this.storeToText(); |
|
250 |
+ } |
|
251 |
+ text = this.wrapped.get(0).value; |
|
252 |
+ if (this.history[this.historyPointer] !== text) { |
|
253 |
+ this.historyTruncate(); |
|
254 |
+ this.history.push(text); |
|
255 |
+ return this.historyPointer += 1; |
|
256 |
+ } |
|
257 |
+ } |
|
258 |
+ }; |
|
259 |
+ |
|
260 |
+ JSONEditor.prototype.fireChange = function() { |
|
261 |
+ return $(this.wrapped).trigger('change'); |
|
262 |
+ }; |
|
263 |
+ |
|
264 |
+ JSONEditor.prototype.historyTruncate = function() { |
|
265 |
+ if (this.historyPointer + 1 < this.history.length) { |
|
266 |
+ return this.history.splice(this.historyPointer + 1, this.history.length - this.historyPointer); |
|
267 |
+ } |
|
268 |
+ }; |
|
269 |
+ |
|
270 |
+ JSONEditor.prototype.storeToText = function() { |
|
271 |
+ return this.wrapped.get(0).value = JSON.stringify(this.json, null, 2); |
|
272 |
+ }; |
|
273 |
+ |
|
274 |
+ JSONEditor.prototype.getJSONText = function() { |
|
275 |
+ this.rebuild(); |
|
276 |
+ return this.wrapped.get(0).value; |
|
277 |
+ }; |
|
278 |
+ |
|
279 |
+ JSONEditor.prototype.getJSON = function() { |
|
280 |
+ this.rebuild(); |
|
281 |
+ return this.json; |
|
282 |
+ }; |
|
283 |
+ |
|
284 |
+ JSONEditor.prototype.rebuild = function(doNotRefreshText) { |
|
285 |
+ var changed, elem; |
|
286 |
+ if (!this.json) { |
|
287 |
+ this.setJsonFromText(); |
|
288 |
+ } |
|
289 |
+ changed = this.haveThingsChanged(); |
|
290 |
+ if (this.json && !doNotRefreshText) { |
|
291 |
+ this.saveState(); |
|
292 |
+ } |
|
293 |
+ this.cleanBuilder(); |
|
294 |
+ this.setJsonFromText(); |
|
295 |
+ this.alreadyFocused = false; |
|
296 |
+ elem = this.build(this.json, this.builder, null, null, this.json); |
|
297 |
+ this.recoverScrollPosition(); |
|
298 |
+ if (elem && elem.text() === '??' && !this.alreadyFocused && this.doAutoFocus) { |
|
299 |
+ this.alreadyFocused = true; |
|
300 |
+ this.doAutoFocus = false; |
|
301 |
+ elem = elem.find('.editable'); |
|
302 |
+ elem.click(); |
|
303 |
+ elem.find('input').focus().select(); |
|
304 |
+ } |
|
305 |
+ if (changed) { |
|
306 |
+ return this.fireChange(); |
|
307 |
+ } |
|
308 |
+ }; |
|
309 |
+ |
|
310 |
+ JSONEditor.prototype.haveThingsChanged = function() { |
|
311 |
+ return this.json && JSON.stringify(this.json, null, 2) !== this.wrapped.get(0).value; |
|
312 |
+ }; |
|
313 |
+ |
|
314 |
+ JSONEditor.prototype.saveScrollPosition = function() { |
|
315 |
+ return this.oldScrollHeight = this.builder.scrollTop(); |
|
316 |
+ }; |
|
317 |
+ |
|
318 |
+ JSONEditor.prototype.recoverScrollPosition = function() { |
|
319 |
+ return this.builder.scrollTop(this.oldScrollHeight); |
|
320 |
+ }; |
|
321 |
+ |
|
322 |
+ JSONEditor.prototype.setJsonFromText = function() { |
|
323 |
+ if (this.wrapped.get(0).value.length === 0) { |
|
324 |
+ this.wrapped.get(0).value = "{}"; |
|
325 |
+ } |
|
326 |
+ try { |
|
327 |
+ this.wrapped.get(0).value = this.wrapped.get(0).value.replace(/((^|[^\\])(\\\\)*)\\n/g, '$1\\\\n').replace(/((^|[^\\])(\\\\)*)\\t/g, '$1\\\\t'); |
|
328 |
+ return this.json = JSON.parse(this.wrapped.get(0).value); |
|
329 |
+ } catch (e) { |
|
330 |
+ return alert("Got bad JSON from text."); |
|
331 |
+ } |
|
332 |
+ }; |
|
333 |
+ |
|
334 |
+ JSONEditor.prototype.checkJsonInText = function() { |
|
335 |
+ try { |
|
336 |
+ JSON.parse(this.wrapped.get(0).value); |
|
232 | 337 |
return true; |
233 |
- } else { |
|
338 |
+ } catch (e) { |
|
234 | 339 |
return false; |
235 | 340 |
} |
236 |
- } |
|
237 |
- } |
|
238 |
- return true; |
|
239 |
-}; |
|
240 |
- |
|
241 |
-JSONEditor.prototype.restore = function() { |
|
242 |
- if (this.history[this.historyPointer]) { |
|
243 |
- this.wrapped.get(0).value = this.history[this.historyPointer]; |
|
244 |
- this.rebuild(true); |
|
245 |
- } |
|
246 |
-}; |
|
247 |
- |
|
248 |
-JSONEditor.prototype.saveState = function(skipStoreText) { |
|
249 |
- if (this.json) { |
|
250 |
- if (!skipStoreText) this.storeToText(); |
|
251 |
- var text = this.wrapped.get(0).value; |
|
252 |
- if (this.history[this.historyPointer] != text) { |
|
253 |
- this.historyTruncate(); |
|
254 |
- this.history.push(text); |
|
255 |
- this.historyPointer += 1; |
|
256 |
- } |
|
257 |
- } |
|
258 |
-}; |
|
259 |
- |
|
260 |
-JSONEditor.prototype.fireChange = function() { |
|
261 |
- $(this.wrapped).trigger('change'); |
|
262 |
-}; |
|
263 |
- |
|
264 |
-JSONEditor.prototype.historyTruncate = function() { |
|
265 |
- if (this.historyPointer + 1 < this.history.length) { |
|
266 |
- this.history.splice(this.historyPointer + 1, this.history.length - this.historyPointer); |
|
267 |
- } |
|
268 |
-}; |
|
269 |
- |
|
270 |
-JSONEditor.prototype.storeToText = function() { |
|
271 |
- this.wrapped.get(0).value = JSON.stringify(this.json, null, 2); |
|
272 |
-}; |
|
273 |
- |
|
274 |
-JSONEditor.prototype.getJSONText = function() { |
|
275 |
- this.rebuild(); |
|
276 |
- return this.wrapped.get(0).value; |
|
277 |
-}; |
|
278 |
- |
|
279 |
-JSONEditor.prototype.getJSON = function() { |
|
280 |
- this.rebuild(); |
|
281 |
- return this.json; |
|
282 |
-}; |
|
283 |
- |
|
284 |
-JSONEditor.prototype.rebuild = function(doNotRefreshText) { |
|
285 |
- if (!this.json) this.setJsonFromText(); |
|
286 |
- var changed = this.haveThingsChanged(); |
|
287 |
- if (this.json && !doNotRefreshText) { |
|
288 |
- this.saveState(); |
|
289 |
- } |
|
290 |
- this.cleanBuilder(); |
|
291 |
- this.setJsonFromText(); |
|
292 |
- this.alreadyFocused = false; |
|
293 |
- var elem = this.build(this.json, this.builder, null, null, this.json); |
|
294 |
- |
|
295 |
- this.recoverScrollPosition(); |
|
296 |
- |
|
297 |
- // Auto-focus to edit '??' keys and values. |
|
298 |
- if (elem) if (elem.text() == '??' && !this.alreadyFocused && this.doAutoFocus) { |
|
299 |
- this.alreadyFocused = true; |
|
300 |
- this.doAutoFocus = false; |
|
301 |
- |
|
302 |
- elem = elem.find('.editable'); |
|
303 |
- elem.click(); |
|
304 |
- elem.find('input').focus().select(); |
|
305 |
- //still missing a proper scrolling into the selected input |
|
306 |
- } |
|
307 |
- |
|
308 |
- if (changed) this.fireChange(); |
|
309 |
-}; |
|
310 |
- |
|
311 |
-JSONEditor.prototype.haveThingsChanged = function() { |
|
312 |
- return (this.json && JSON.stringify(this.json, null, 2) != this.wrapped.get(0).value); |
|
313 |
-} |
|
314 |
- |
|
315 |
-JSONEditor.prototype.saveScrollPosition = function() { |
|
316 |
- this.oldScrollHeight = this.builder.scrollTop(); |
|
317 |
-}; |
|
318 |
- |
|
319 |
-JSONEditor.prototype.recoverScrollPosition = function() { |
|
320 |
- this.builder.scrollTop(this.oldScrollHeight); |
|
321 |
-}; |
|
322 |
- |
|
323 |
-JSONEditor.prototype.setJsonFromText = function() { |
|
324 |
- if (this.wrapped.get(0).value.length == 0) this.wrapped.get(0).value = "{}"; |
|
325 |
- try { |
|
326 |
- this.wrapped.get(0).value = this.wrapped.get(0).value.replace(/((^|[^\\])(\\\\)*)\\n/g, '$1\\\\n').replace(/((^|[^\\])(\\\\)*)\\t/g, '$1\\\\t'); |
|
327 |
- this.json = JSON.parse(this.wrapped.get(0).value); |
|
328 |
- } catch(e) { |
|
329 |
- alert("Got bad JSON from text."); |
|
330 |
- } |
|
331 |
-}; |
|
332 |
- |
|
333 |
-JSONEditor.prototype.checkJsonInText = function() { |
|
334 |
- try { |
|
335 |
- JSON.parse(this.wrapped.get(0).value); |
|
336 |
- return true; |
|
337 |
- } catch(e) { |
|
338 |
- return false; |
|
339 |
- } |
|
340 |
-}; |
|
341 |
- |
|
342 |
-JSONEditor.prototype.logJSON = function() { |
|
343 |
- console.log(JSON.stringify(this.json, null, 2)); |
|
344 |
-}; |
|
345 |
- |
|
346 |
-JSONEditor.prototype.cleanBuilder = function() { |
|
347 |
- if (!this.builder) { |
|
348 |
- this.builder = $('<div class="builder"></div>'); |
|
349 |
- this.container.append(this.builder); |
|
350 |
- } |
|
351 |
- this.saveScrollPosition(); |
|
352 |
- this.builder.text(''); |
|
353 |
- |
|
354 |
- this.builder.css("position", "absolute").css("top", 0).css("left", 0); |
|
355 |
- this.builder.width(this.wrapped.width()).height(this.wrapped.height()); |
|
356 |
- this.wrapped.css("position", "absolute").css("top", 0).css("left", 0); |
|
357 |
- this.showFunctionButtons("defined"); |
|
358 |
-}; |
|
359 |
- |
|
360 |
-JSONEditor.prototype.updateStruct = function(struct, key, val, kind, selectionStart, selectionEnd) { |
|
361 |
- if(kind == 'key') { |
|
362 |
- if (selectionStart && selectionEnd) val = key.substring(0, selectionStart) + val + key.substring(selectionEnd, key.length); |
|
363 |
- struct[val] = struct[key]; |
|
364 |
- |
|
365 |
- //order keys |
|
366 |
- var orderrest = 0; |
|
367 |
- $.each(struct, function (index, value) { |
|
368 |
- //re set rest of the keys |
|
369 |
- if(orderrest & index != val) { |
|
370 |
- var tempval = struct[index]; |
|
371 |
- delete struct[index]; |
|
372 |
- struct[index] = tempval; |
|
373 |
- } |
|
374 |
- if(key == index) { |
|
375 |
- orderrest = 1; |
|
376 |
- } |
|
377 |
- }); |
|
378 |
- // end of order keys |
|
379 |
- |
|
380 |
- if (key != val) delete struct[key]; |
|
381 |
- } else { |
|
382 |
- if (selectionStart && selectionEnd) val = struct[key].substring(0, selectionStart) + val + struct[key].substring(selectionEnd, struct[key].length); |
|
383 |
- struct[key] = val; |
|
384 |
- } |
|
385 |
-}; |
|
386 |
- |
|
387 |
-JSONEditor.prototype.getValFromStruct = function(struct, key, kind) { |
|
388 |
- if(kind == 'key') { |
|
389 |
- return key; |
|
390 |
- } else { |
|
391 |
- return struct[key]; |
|
392 |
- } |
|
393 |
-}; |
|
394 |
- |
|
395 |
-JSONEditor.prototype.doTruncation = function(trueOrFalse) { |
|
396 |
- if (this._doTruncation != trueOrFalse) { |
|
397 |
- this._doTruncation = trueOrFalse; |
|
398 |
- this.rebuild(); |
|
399 |
- } |
|
400 |
-}; |
|
401 |
- |
|
402 |
-JSONEditor.prototype.showWipe = function(trueOrFalse) { |
|
403 |
- if (this._showWipe != trueOrFalse) { |
|
404 |
- this._showWipe = trueOrFalse; |
|
405 |
- this.rebuild(); |
|
406 |
- } |
|
407 |
-}; |
|
408 |
- |
|
409 |
-JSONEditor.prototype.truncate = function(text, length) { |
|
410 |
- if (text.length == 0) return '-empty-'; |
|
411 |
- if(this._doTruncation && text.length > (length || 30)) return(text.substring(0, (length || 30)) + '...'); |
|
412 |
- return text; |
|
413 |
-}; |
|
414 |
- |
|
415 |
-JSONEditor.prototype.replaceLastSelectedFieldIfRecent = function(text) { |
|
416 |
- if (this.lastEditingUnfocusedTime > (new Date()).getTime() - 200) { // Short delay for unfocus to occur. |
|
417 |
- this.setLastEditingFocus(text); |
|
418 |
- this.rebuild(); |
|
419 |
- } |
|
420 |
-}; |
|
421 |
- |
|
422 |
-JSONEditor.prototype.editingUnfocused = function(elem, struct, key, root, kind) { |
|
423 |
- var self = this; |
|
424 |
- |
|
425 |
- var selectionStart = elem && elem.target.selectionStart; |
|
426 |
- var selectionEnd = elem && elem.target.selectionEnd; |
|
427 |
- |
|
428 |
- this.setLastEditingFocus = function(text) { |
|
429 |
- self.updateStruct(struct, key, text, kind, selectionStart, selectionEnd); |
|
430 |
- self.json = root; // Because self.json is a new reference due to rebuild. |
|
431 |
- }; |
|
432 |
- this.lastEditingUnfocusedTime = (new Date()).getTime(); |
|
433 |
-}; |
|
434 |
- |
|
435 |
-JSONEditor.prototype.edit = function(e, key, struct, root, kind){ |
|
436 |
- var self = this; |
|
437 |
- var form = $("<form></form>").css('display', 'inline'); |
|
438 |
- var input = document.createElement("INPUT"); |
|
439 |
- input.value = this.getValFromStruct(struct, key, kind); |
|
440 |
- //alert(this.getValFromStruct(struct, key, kind)); |
|
441 |
- input.className = 'edit_field'; |
|
442 |
- var onblur = function(elem) { |
|
443 |
- var val = input.value; |
|
444 |
- self.updateStruct(struct, key, val, kind); |
|
445 |
- self.editingUnfocused(elem, struct, (kind == 'key' ? val : key), root, kind); |
|
446 |
- e.text(self.truncate(val)); |
|
447 |
- e.get(0).editing = false; |
|
448 |
- if (key != val) self.rebuild(); |
|
449 |
- return false; |
|
450 |
- }; |
|
451 |
- $(input).blur(onblur); |
|
452 |
- $(input).keydown(function(e) { |
|
453 |
- if (e.keyCode == 9 || e.keyCode == 13) { // Tab and enter |
|
454 |
- self.doAutoFocus = true; |
|
455 |
- onblur(e); |
|
456 |
- return false; |
|
457 |
- } |
|
458 |
- }); |
|
459 |
- $(form).submit(function(e) { self.doAutoFocus = true; onblur(e); return false;}).append(input); |
|
460 |
- $(e).html(form); |
|
461 |
- input.focus(); |
|
462 |
-}; |
|
463 |
- |
|
464 |
-JSONEditor.prototype.editable = function(text, key, struct, root, kind) { |
|
465 |
- var self = this; |
|
466 |
- var elem = $('<span class="editable" href="#"></span>').text(this.truncate(text)).click(function(e) { |
|
467 |
- if (!this.editing) { |
|
468 |
- this.editing = true; |
|
469 |
- self.edit($(this), key, struct, root, kind); |
|
470 |
- } |
|
471 |
- return true; |
|
472 |
- }); |
|
473 |
- |
|
474 |
- return elem; |
|
475 |
-} |
|
476 |
- |
|
477 |
-JSONEditor.prototype.build = function(json, node, parent, key, root) { |
|
478 |
- var elem = null; |
|
479 |
- if(json instanceof Array){ |
|
480 |
- var bq = $(document.createElement("BLOCKQUOTE")); |
|
481 |
- bq.append($('<div class="brackets">[</div>')); |
|
482 |
- |
|
483 |
- bq.prepend(this.addUI(json)); |
|
484 |
- if (parent) { |
|
485 |
- if (this._showWipe) bq.prepend(this.wipeUI(key, parent)); |
|
486 |
- bq.prepend(this.deleteUI(key, parent)); |
|
487 |
- } |
|
341 |
+ }; |
|
488 | 342 |
|
489 |
- for(var i = 0; i < json.length; i++) { |
|
490 |
- var innerbq = $(document.createElement("BLOCKQUOTE")); |
|
491 |
- var newElem = this.build(json[i], innerbq, json, i, root); |
|
492 |
- if (newElem) if (newElem.text() == "??") elem = newElem; |
|
493 |
- bq.append(innerbq); |
|
494 |
- } |
|
343 |
+ JSONEditor.prototype.logJSON = function() { |
|
344 |
+ return console.log(JSON.stringify(this.json, null, 2)); |
|
345 |
+ }; |
|
495 | 346 |
|
496 |
- bq.append($('<div class="brackets">]</div>')); |
|
497 |
- node.append(bq); |
|
498 |
- } else if (json instanceof Object) { |
|
499 |
- var bq = $(document.createElement("BLOCKQUOTE")); |
|
500 |
- bq.append($('<div class="bracers">{</div>')); |
|
501 |
- |
|
502 |
- for(var i in json){ |
|
503 |
- var innerbq = $(document.createElement("BLOCKQUOTE")); |
|
504 |
- var newElem = this.editable(i.toString(), i.toString(), json, root, 'key').wrap('<span class="key"></span>').parent(); |
|
505 |
- innerbq.append(newElem); |
|
506 |
- if (newElem) if (newElem.text() == "??") elem = newElem; |
|
507 |
- if (typeof json[i] != 'string' && typeof json[i] != 'number') { |
|
508 |
- innerbq.prepend(this.braceUI(i, json)); |
|
509 |
- innerbq.prepend(this.bracketUI(i, json)); |
|
510 |
- if (this._showWipe) innerbq.prepend(this.wipeUI(i, json)); |
|
511 |
- innerbq.prepend(this.deleteUI(i, json, true)); |
|
347 |
+ JSONEditor.prototype.cleanBuilder = function() { |
|
348 |
+ if (!this.builder) { |
|
349 |
+ this.builder = $('<div class="builder"></div>'); |
|
350 |
+ this.container.append(this.builder); |
|
512 | 351 |
} |
513 |
- innerbq.append($('<span class="colon">: </span>')); |
|
514 |
- newElem = this.build(json[i], innerbq, json, i, root); |
|
515 |
- if (newElem) if (newElem.text() == "??") elem = newElem; |
|
516 |
- bq.append(innerbq); |
|
517 |
- } |
|
352 |
+ this.saveScrollPosition(); |
|
353 |
+ this.builder.text(''); |
|
354 |
+ return this.showFunctionButtons("defined"); |
|
355 |
+ }; |
|
356 |
+ |
|
357 |
+ JSONEditor.prototype.updateStruct = function(struct, key, val, kind, selectionStart, selectionEnd) { |
|
358 |
+ var orderrest; |
|
359 |
+ if (kind === 'key') { |
|
360 |
+ if (selectionStart && selectionEnd) { |
|
361 |
+ val = key.substring(0, selectionStart) + val + key.substring(selectionEnd, key.length); |
|
362 |
+ } |
|
363 |
+ struct[val] = struct[key]; |
|
364 |
+ orderrest = 0; |
|
365 |
+ $.each(struct, function(index, value) { |
|
366 |
+ var tempval; |
|
367 |
+ if (orderrest & index !== val) { |
|
368 |
+ tempval = struct[index]; |
|
369 |
+ delete struct[index]; |
|
370 |
+ struct[index] = tempval; |
|
371 |
+ } |
|
372 |
+ if (key === index) { |
|
373 |
+ return orderrest = 1; |
|
374 |
+ } |
|
375 |
+ }); |
|
376 |
+ if (key !== val) { |
|
377 |
+ return delete struct[key]; |
|
378 |
+ } |
|
379 |
+ } else { |
|
380 |
+ if (selectionStart && selectionEnd) { |
|
381 |
+ val = struct[key].substring(0, selectionStart) + val + struct[key].substring(selectionEnd, struct[key].length); |
|
382 |
+ } |
|
383 |
+ return struct[key] = val; |
|
384 |
+ } |
|
385 |
+ }; |
|
518 | 386 |
|
519 |
- bq.prepend(this.addUI(json)); |
|
520 |
- if (parent) { |
|
521 |
- if (this._showWipe) bq.prepend(this.wipeUI(key, parent)); |
|
522 |
- bq.prepend(this.deleteUI(key, parent)); |
|
523 |
- } |
|
387 |
+ JSONEditor.prototype.getValFromStruct = function(struct, key, kind) { |
|
388 |
+ if (kind === 'key') { |
|
389 |
+ return key; |
|
390 |
+ } else { |
|
391 |
+ return struct[key]; |
|
392 |
+ } |
|
393 |
+ }; |
|
524 | 394 |
|
525 |
- bq.append($('<div class="bracers">}</div>')); |
|
526 |
- node.append(bq); |
|
527 |
- } else { |
|
528 |
- elem = this.editable(json.toString(), key, parent, root, 'value').wrap('<span class="val"></span>').parent(); |
|
529 |
- node.append(elem); |
|
530 |
- node.prepend(this.braceUI(key, parent)); |
|
531 |
- node.prepend(this.bracketUI(key, parent)); |
|
532 |
- if (parent) { |
|
533 |
- if (this._showWipe) node.prepend(this.wipeUI(key, parent)); |
|
534 |
- node.prepend(this.deleteUI(key, parent)); |
|
535 |
- } |
|
395 |
+ JSONEditor.prototype.doTruncation = function(trueOrFalse) { |
|
396 |
+ if (this._doTruncation !== trueOrFalse) { |
|
397 |
+ this._doTruncation = trueOrFalse; |
|
398 |
+ return this.rebuild(); |
|
399 |
+ } |
|
400 |
+ }; |
|
401 |
+ |
|
402 |
+ JSONEditor.prototype.showWipe = function(trueOrFalse) { |
|
403 |
+ if (this._showWipe !== trueOrFalse) { |
|
404 |
+ this._showWipe = trueOrFalse; |
|
405 |
+ return this.rebuild(); |
|
406 |
+ } |
|
407 |
+ }; |
|
408 |
+ |
|
409 |
+ JSONEditor.prototype.truncate = function(text, length) { |
|
410 |
+ if (text.length === 0) { |
|
411 |
+ return '-empty-'; |
|
412 |
+ } |
|
413 |
+ if (this._doTruncation && text.length > (length || 30)) { |
|
414 |
+ return text.substring(0, length || 30) + '...'; |
|
415 |
+ } |
|
416 |
+ return text; |
|
417 |
+ }; |
|
418 |
+ |
|
419 |
+ JSONEditor.prototype.replaceLastSelectedFieldIfRecent = function(text) { |
|
420 |
+ if (this.lastEditingUnfocusedTime > (new Date()).getTime() - 200) { |
|
421 |
+ this.setLastEditingFocus(text); |
|
422 |
+ return this.rebuild(); |
|
423 |
+ } |
|
424 |
+ }; |
|
425 |
+ |
|
426 |
+ JSONEditor.prototype.editingUnfocused = function(elem, struct, key, root, kind) { |
|
427 |
+ var selectionEnd, selectionStart, |
|
428 |
+ _this = this; |
|
429 |
+ selectionStart = elem != null ? elem.selectionStart : void 0; |
|
430 |
+ selectionEnd = elem != null ? elem.selectionEnd : void 0; |
|
431 |
+ this.setLastEditingFocus = function(text) { |
|
432 |
+ _this.updateStruct(struct, key, text, kind, selectionStart, selectionEnd); |
|
433 |
+ return _this.json = root; |
|
434 |
+ }; |
|
435 |
+ return this.lastEditingUnfocusedTime = (new Date()).getTime(); |
|
436 |
+ }; |
|
437 |
+ |
|
438 |
+ JSONEditor.prototype.edit = function($elem, key, struct, root, kind) { |
|
439 |
+ var $input, blurHandler, form, |
|
440 |
+ _this = this; |
|
441 |
+ form = $("<form></form>").css('display', 'inline'); |
|
442 |
+ $input = $("<input />"); |
|
443 |
+ $input.val(this.getValFromStruct(struct, key, kind)); |
|
444 |
+ $input.addClass('edit_field'); |
|
445 |
+ blurHandler = function() { |
|
446 |
+ var val, _ref; |
|
447 |
+ val = $input.val(); |
|
448 |
+ _this.updateStruct(struct, key, val, kind); |
|
449 |
+ _this.editingUnfocused($elem, struct, (_ref = kind === 'key') != null ? _ref : { |
|
450 |
+ val: key |
|
451 |
+ }, root, kind); |
|
452 |
+ $elem.text(_this.truncate(val)); |
|
453 |
+ $elem.get(0).editing = false; |
|
454 |
+ if (key !== val) { |
|
455 |
+ return _this.rebuild(); |
|
456 |
+ } |
|
457 |
+ }; |
|
458 |
+ $input.blur(blurHandler); |
|
459 |
+ $input.keydown(function(e) { |
|
460 |
+ if (e.keyCode === 9 || e.keyCode === 13) { |
|
461 |
+ _this.doAutoFocus = true; |
|
462 |
+ return blurHandler(); |
|
463 |
+ } |
|
464 |
+ }); |
|
465 |
+ $(form).append($input).submit(function(e) { |
|
466 |
+ e.preventDefault(); |
|
467 |
+ _this.doAutoFocus = true; |
|
468 |
+ return blurHandler(); |
|
469 |
+ }); |
|
470 |
+ $elem.html(form); |
|
471 |
+ return $input.focus(); |
|
472 |
+ }; |
|
473 |
+ |
|
474 |
+ JSONEditor.prototype.editable = function(text, key, struct, root, kind) { |
|
475 |
+ var elem, self; |
|
476 |
+ self = this; |
|
477 |
+ elem = $('<span class="editable" href="#"></span>').text(this.truncate(text)).click(function(e) { |
|
478 |
+ if (!this.editing) { |
|
479 |
+ this.editing = true; |
|
480 |
+ self.edit($(this), key, struct, root, kind); |
|
481 |
+ } |
|
482 |
+ return true; |
|
483 |
+ }); |
|
484 |
+ return elem; |
|
485 |
+ }; |
|
486 |
+ |
|
487 |
+ JSONEditor.prototype.build = function(json, node, parent, key, root) { |
|
488 |
+ var bq, elem, i, innerbq, jsonkey, jsonvalue, newElem, _i, _ref; |
|
489 |
+ elem = null; |
|
490 |
+ if (json instanceof Array) { |
|
491 |
+ bq = $(document.createElement("BLOCKQUOTE")); |
|
492 |
+ bq.append($('<div class="brackets">[</div>')); |
|
493 |
+ bq.prepend(this.addUI(json)); |
|
494 |
+ if (parent) { |
|
495 |
+ if (this._showWipe) { |
|
496 |
+ bq.prepend(this.wipeUI(key, parent)); |
|
497 |
+ } |
|
498 |
+ bq.prepend(this.deleteUI(key, parent)); |
|
499 |
+ } |
|
500 |
+ for (i = _i = 0, _ref = json.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { |
|
501 |
+ innerbq = $(document.createElement("BLOCKQUOTE")); |
|
502 |
+ newElem = this.build(json[i], innerbq, json, i, root); |
|
503 |
+ if (newElem && newElem.text() === "??") { |
|
504 |
+ elem = newElem; |
|
505 |
+ } |
|
506 |
+ bq.append(innerbq); |
|
507 |
+ } |
|
508 |
+ bq.append($('<div class="brackets">]</div>')); |
|
509 |
+ node.append(bq); |
|
510 |
+ } else if (json instanceof Object) { |
|
511 |
+ bq = $(document.createElement("BLOCKQUOTE")); |
|
512 |
+ bq.append($('<div class="bracers">{</div>')); |
|
513 |
+ for (jsonkey in json) { |
|
514 |
+ jsonvalue = json[jsonkey]; |
|
515 |
+ innerbq = $(document.createElement("BLOCKQUOTE")); |
|
516 |
+ newElem = this.editable(jsonkey.toString(), jsonkey.toString(), json, root, 'key').wrap('<span class="key"></b>').parent(); |
|
517 |
+ innerbq.append(newElem); |
|
518 |
+ if (newElem && newElem.text() === "??") { |
|
519 |
+ elem = newElem; |
|
520 |
+ } |
|
521 |
+ if (typeof jsonvalue !== 'string') { |
|
522 |
+ innerbq.prepend(this.braceUI(jsonkey, json)); |
|
523 |
+ innerbq.prepend(this.bracketUI(jsonkey, json)); |
|
524 |
+ if (this._showWipe) { |
|
525 |
+ innerbq.prepend(this.wipeUI(jsonkey, json)); |
|
526 |
+ } |
|
527 |
+ innerbq.prepend(this.deleteUI(jsonkey, json, true)); |
|
528 |
+ } |
|
529 |
+ innerbq.append($('<span class="colon">: </span>')); |
|
530 |
+ newElem = this.build(jsonvalue, innerbq, json, jsonkey, root); |
|
531 |
+ if (newElem && newElem.text() === "??") { |
|
532 |
+ elem = newElem; |
|
533 |
+ } |
|
534 |
+ bq.append(innerbq); |
|
535 |
+ } |
|
536 |
+ bq.prepend(this.addUI(json)); |
|
537 |
+ if (parent) { |
|
538 |
+ if (this._showWipe) { |
|
539 |
+ bq.prepend(this.wipeUI(key, parent)); |
|
540 |
+ } |
|
541 |
+ bq.prepend(this.deleteUI(key, parent)); |
|
542 |
+ } |
|
543 |
+ bq.append($('<div class="bracers">}</div>')); |
|
544 |
+ node.append(bq); |
|
545 |
+ } else { |
|
546 |
+ elem = this.editable(json.toString(), key, parent, root, 'value').wrap('<span class="val"></span>').parent(); |
|
547 |
+ node.append(elem); |
|
548 |
+ node.prepend(this.braceUI(key, parent)); |
|
549 |
+ node.prepend(this.bracketUI(key, parent)); |
|
550 |
+ if (parent) { |
|
551 |
+ if (this._showWipe) { |
|
552 |
+ node.prepend(this.wipeUI(key, parent)); |
|
553 |
+ } |
|
554 |
+ node.prepend(this.deleteUI(key, parent)); |
|
555 |
+ } |
|
556 |
+ } |
|
557 |
+ return elem; |
|
558 |
+ }; |
|
559 |
+ |
|
560 |
+ return JSONEditor; |
|
561 |
+ |
|
562 |
+ })(); |
|
536 | 563 |
|
537 |
- } |
|
538 |
- return elem; |
|
539 |
-}; |
|
564 |
+}).call(this); |
@@ -0,0 +1,37 @@ |
||
1 |
+.json-editor { |
|
2 |
+ background-color: #FFF; |
|
3 |
+ position: relative; } |
|
4 |
+ .json-editor textarea { |
|
5 |
+ width: 100%; |
|
6 |
+ font-family: monospace; } |
|
7 |
+ .json-editor .builder { |
|
8 |
+ background-color: white; |
|
9 |
+ overflow: auto; |
|
10 |
+ font-size: 0.9em; } |
|
11 |
+ .json-editor .builder .key { |
|
12 |
+ font-weight: bold; } |
|
13 |
+ .json-editor .builder .key .edit_field { |
|
14 |
+ width: 150px; } |
|
15 |
+ .json-editor .builder .val .edit_field { |
|
16 |
+ width: 200px; } |
|
17 |
+ .json-editor blockquote { |
|
18 |
+ margin: 0; |
|
19 |
+ padding: 0; |
|
20 |
+ clear: both; |
|
21 |
+ padding-left: 7px; } |
|
22 |
+ .json-editor div { |
|
23 |
+ background-color: #cfc; |
|
24 |
+ margin: 1px; |
|
25 |
+ padding: 2px; } |
|
26 |
+ .json-editor .val { |
|
27 |
+ font-style: italic; } |
|
28 |
+ .json-editor .key a, .json-editor .val a { |
|
29 |
+ color: black; |
|
30 |
+ text-decoration: none; } |
|
31 |
+ .json-editor .icon { |
|
32 |
+ display: block; |
|
33 |
+ float: right; |
|
34 |
+ text-decoration: none; |
|
35 |
+ padding-left: 5px; |
|
36 |
+ border: 0; |
|
37 |
+ color: blue; } |
@@ -1,63 +0,0 @@ |
||
1 |
-.json-editor { |
|
2 |
- background-color: #FFF; |
|
3 |
- position: relative; |
|
4 |
- |
|
5 |
- .builder { |
|
6 |
- background-color: white; |
|
7 |
- overflow: auto; |
|
8 |
- font-size: 0.9em; |
|
9 |
- padding-right: 10px; |
|
10 |
- |
|
11 |
- .key { |
|
12 |
- font-weight: bold; |
|
13 |
- |
|
14 |
- .edit_field { |
|
15 |
- width: 80px; |
|
16 |
- } |
|
17 |
- |
|
18 |
- a { |
|
19 |
- color: black; |
|
20 |
- text-decoration: none; |
|
21 |
- } |
|
22 |
- } |
|
23 |
- |
|
24 |
- .val { |
|
25 |
- font-style: italic; |
|
26 |
- |
|
27 |
- .edit_field { |
|
28 |
- width: 180px; |
|
29 |
- } |
|
30 |
- |
|
31 |
- a { |
|
32 |
- color: black; |
|
33 |
- text-decoration: none; |
|
34 |
- } |
|
35 |
- } |
|
36 |
- } |
|
37 |
- |
|
38 |
- blockquote { |
|
39 |
- margin: 0; |
|
40 |
- padding: 0; |
|
41 |
- clear: both; |
|
42 |
- padding-left: 7px; |
|
43 |
- } |
|
44 |
- |
|
45 |
- div { |
|
46 |
- background-color: #cfc; |
|
47 |
- margin: 1px; |
|
48 |
- padding: 2px; |
|
49 |
- } |
|
50 |
- |
|
51 |
- .icon { |
|
52 |
- display: block; |
|
53 |
- float: right; |
|
54 |
- text-decoration: none; |
|
55 |
- padding: 0 5px; |
|
56 |
- border: 0 !important; |
|
57 |
- color: blue; |
|
58 |
- |
|
59 |
- &:hover { |
|
60 |
- background-color: #bbb; |
|
61 |
- } |
|
62 |
- } |
|
63 |
-} |